From 74058ac5f16b2eea391094ea794aa9c9a6f9dd2f Mon Sep 17 00:00:00 2001 From: jules Date: Tue, 9 Sep 2008 13:12:46 +0000 Subject: [PATCH] --- .../juce_win32_ASIO.cpp | 2 +- .../juce_OggVorbisAudioFormat.cpp | 1021 ++-- .../audio/midi/juce_MidiMessage.cpp | 1 - .../audio/midi/juce_MidiMessageCollector.cpp | 6 +- .../gui/components/controls/juce_Label.cpp | 900 +-- .../gui/components/controls/juce_Label.h | 8 +- .../gui/components/controls/juce_Slider.cpp | 2734 ++++----- .../components/controls/juce_TextEditor.cpp | 5244 +++++++++-------- .../gui/components/controls/juce_TextEditor.h | 1391 ++--- .../properties/juce_PropertyPanel.cpp | 863 +-- .../properties/juce_PropertyPanel.h | 6 + .../image_file_formats/juce_JPEGLoader.cpp | 754 +-- src/juce_core/basics/juce_StandardHeader.h | 1 + src/juce_core/containers/juce_BitArray.cpp | 2 +- src/juce_core/io/files/juce_File.cpp | 6 - src/juce_core/text/juce_String.cpp | 6 +- 16 files changed, 6538 insertions(+), 6407 deletions(-) diff --git a/build/win32/platform_specific_code/juce_win32_ASIO.cpp b/build/win32/platform_specific_code/juce_win32_ASIO.cpp index 7b96e76700..81043ec29d 100644 --- a/build/win32/platform_specific_code/juce_win32_ASIO.cpp +++ b/build/win32/platform_specific_code/juce_win32_ASIO.cpp @@ -833,7 +833,7 @@ public: AudioIODeviceCallback* const oldCallback = currentCallback; close(); - open (currentChansIn, currentChansOut, + open (BitArray (currentChansIn), BitArray (currentChansOut), currentSampleRate, currentBlockSizeSamples); if (oldCallback != 0) diff --git a/src/juce_appframework/audio/audio_file_formats/juce_OggVorbisAudioFormat.cpp b/src/juce_appframework/audio/audio_file_formats/juce_OggVorbisAudioFormat.cpp index c43d4cbf0c..9756e2cf01 100644 --- a/src/juce_appframework/audio/audio_file_formats/juce_OggVorbisAudioFormat.cpp +++ b/src/juce_appframework/audio/audio_file_formats/juce_OggVorbisAudioFormat.cpp @@ -1,501 +1,520 @@ -/* - ============================================================================== - - This file is part of the JUCE library - "Jules' Utility Class Extensions" - Copyright 2004-7 by Raw Material Software ltd. - - ------------------------------------------------------------------------------ - - JUCE can be redistributed and/or modified 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. - - JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the - Free Software Foundation, Inc., 59 Temple Place, Suite 330, - Boston, MA 02111-1307 USA - - ------------------------------------------------------------------------------ - - If you'd like to release a closed-source product which uses JUCE, commercial - licenses are also available: visit www.rawmaterialsoftware.com/juce for - more information. - - ============================================================================== -*/ - -#include "../../../../juce_Config.h" - -#if JUCE_USE_OGGVORBIS - -#include "../../../juce_core/basics/juce_StandardHeader.h" - -#if JUCE_MAC - #define __MACOSX__ 1 -#endif - - -namespace OggVorbisNamespace -{ -#include "oggvorbis/vorbisenc.h" -#include "oggvorbis/codec.h" -#include "oggvorbis/vorbisfile.h" - -#include "oggvorbis/bitwise.c" -#include "oggvorbis/framing.c" -#include "oggvorbis/libvorbis-1.1.2/lib/analysis.c" -#include "oggvorbis/libvorbis-1.1.2/lib/bitrate.c" -#include "oggvorbis/libvorbis-1.1.2/lib/block.c" -#include "oggvorbis/libvorbis-1.1.2/lib/codebook.c" -#include "oggvorbis/libvorbis-1.1.2/lib/envelope.c" -#include "oggvorbis/libvorbis-1.1.2/lib/floor0.c" -#include "oggvorbis/libvorbis-1.1.2/lib/floor1.c" -#include "oggvorbis/libvorbis-1.1.2/lib/info.c" -#include "oggvorbis/libvorbis-1.1.2/lib/lpc.c" -#include "oggvorbis/libvorbis-1.1.2/lib/lsp.c" -#include "oggvorbis/libvorbis-1.1.2/lib/mapping0.c" -#include "oggvorbis/libvorbis-1.1.2/lib/mdct.c" -#include "oggvorbis/libvorbis-1.1.2/lib/psy.c" -#include "oggvorbis/libvorbis-1.1.2/lib/registry.c" -#include "oggvorbis/libvorbis-1.1.2/lib/res0.c" -#include "oggvorbis/libvorbis-1.1.2/lib/sharedbook.c" -#include "oggvorbis/libvorbis-1.1.2/lib/smallft.c" -#include "oggvorbis/libvorbis-1.1.2/lib/synthesis.c" -#include "oggvorbis/libvorbis-1.1.2/lib/vorbisenc.c" -#include "oggvorbis/libvorbis-1.1.2/lib/vorbisfile.c" -#include "oggvorbis/libvorbis-1.1.2/lib/window.c" -} - -BEGIN_JUCE_NAMESPACE - -#include "juce_OggVorbisAudioFormat.h" -#include "../../application/juce_Application.h" -#include "../../../juce_core/basics/juce_Random.h" -#include "../../../juce_core/io/files/juce_FileInputStream.h" -#include "../../../juce_core/text/juce_LocalisedStrings.h" - -using namespace OggVorbisNamespace; - -//============================================================================== -#define oggFormatName TRANS("Ogg-Vorbis file") -static const tchar* const oggExtensions[] = { T(".ogg"), 0 }; - - -//============================================================================== -class OggReader : public AudioFormatReader -{ - OggVorbis_File ovFile; - ov_callbacks callbacks; - AudioSampleBuffer reservoir; - int reservoirStart, samplesInReservoir; - -public: - //============================================================================== - OggReader (InputStream* const inp) - : AudioFormatReader (inp, oggFormatName), - reservoir (2, 4096), - reservoirStart (0), - samplesInReservoir (0) - { - sampleRate = 0; - usesFloatingPointData = true; - - callbacks.read_func = &oggReadCallback; - callbacks.seek_func = &oggSeekCallback; - callbacks.close_func = &oggCloseCallback; - callbacks.tell_func = &oggTellCallback; - - const int err = ov_open_callbacks (input, &ovFile, 0, 0, callbacks); - - if (err == 0) - { - vorbis_info* info = ov_info (&ovFile, -1); - lengthInSamples = (uint32) ov_pcm_total (&ovFile, -1); - numChannels = info->channels; - bitsPerSample = 16; - sampleRate = info->rate; - - reservoir.setSize (numChannels, - (int) jmin (lengthInSamples, (int64) reservoir.getNumSamples())); - } - } - - ~OggReader() - { - ov_clear (&ovFile); - } - - //============================================================================== - bool read (int** destSamples, - int64 startSampleInFile, - int numSamples) - { - int writeOffset = 0; - - while (numSamples > 0) - { - const int numAvailable = reservoirStart + samplesInReservoir - startSampleInFile; - - if (startSampleInFile >= reservoirStart && numAvailable > 0) - { - // got a few samples overlapping, so use them before seeking.. - - const int numToUse = jmin (numSamples, numAvailable); - - for (unsigned int i = 0; i < numChannels; ++i) - { - if (destSamples[i] == 0) - break; - - memcpy (destSamples[i] + writeOffset, - reservoir.getSampleData (jmin (i, reservoir.getNumChannels()), - (int) (startSampleInFile - reservoirStart)), - sizeof (float) * numToUse); - } - - startSampleInFile += numToUse; - numSamples -= numToUse; - writeOffset += numToUse; - - if (numSamples == 0) - break; - } - - if (startSampleInFile < reservoirStart - || startSampleInFile + numSamples > reservoirStart + samplesInReservoir) - { - // buffer miss, so refill the reservoir - int bitStream = 0; - - reservoirStart = jmax (0, (int) startSampleInFile); - samplesInReservoir = reservoir.getNumSamples(); - - if (reservoirStart != (int) ov_pcm_tell (&ovFile)) - ov_pcm_seek (&ovFile, reservoirStart); - - int offset = 0; - int numToRead = samplesInReservoir; - - while (numToRead > 0) - { - float** dataIn = 0; - - const int samps = ov_read_float (&ovFile, &dataIn, numToRead, &bitStream); - if (samps == 0) - break; - - jassert (samps <= numToRead); - - for (int i = jmin (numChannels, reservoir.getNumChannels()); --i >= 0;) - { - memcpy (reservoir.getSampleData (i, offset), - dataIn[i], - sizeof (float) * samps); - } - - numToRead -= samps; - offset += samps; - } - - if (numToRead > 0) - reservoir.clear (offset, numToRead); - } - } - - return true; - } - - //============================================================================== - static size_t oggReadCallback (void* ptr, size_t size, size_t nmemb, void* datasource) - { - return (size_t) (((InputStream*) datasource)->read (ptr, (int) (size * nmemb)) / size); - } - - static int oggSeekCallback (void* datasource, ogg_int64_t offset, int whence) - { - InputStream* const in = (InputStream*) datasource; - - if (whence == SEEK_CUR) - offset += in->getPosition(); - else if (whence == SEEK_END) - offset += in->getTotalLength(); - - in->setPosition (offset); - return 0; - } - - static int oggCloseCallback (void*) - { - return 0; - } - - static long oggTellCallback (void* datasource) - { - return (long) ((InputStream*) datasource)->getPosition(); - } - - juce_UseDebuggingNewOperator -}; - -//============================================================================== -class OggWriter : public AudioFormatWriter -{ - ogg_stream_state os; - ogg_page og; - ogg_packet op; - vorbis_info vi; - vorbis_comment vc; - vorbis_dsp_state vd; - vorbis_block vb; - -public: - bool ok; - - //============================================================================== - OggWriter (OutputStream* const out, - const double sampleRate, - const int numChannels, - const int bitsPerSample, - const int qualityIndex) - : AudioFormatWriter (out, oggFormatName, - sampleRate, - numChannels, - bitsPerSample) - { - ok = false; - - vorbis_info_init (&vi); - - if (vorbis_encode_init_vbr (&vi, - numChannels, - (int) sampleRate, - jlimit (0.0f, 1.0f, qualityIndex * 0.5f)) == 0) - { - vorbis_comment_init (&vc); - - if (JUCEApplication::getInstance() != 0) - vorbis_comment_add_tag (&vc, "ENCODER", - (char*) (const char*) JUCEApplication::getInstance()->getApplicationName()); - - vorbis_analysis_init (&vd, &vi); - vorbis_block_init (&vd, &vb); - - ogg_stream_init (&os, Random::getSystemRandom().nextInt()); - - ogg_packet header; - ogg_packet header_comm; - ogg_packet header_code; - - vorbis_analysis_headerout (&vd, &vc, &header, &header_comm, &header_code); - - ogg_stream_packetin (&os, &header); - ogg_stream_packetin (&os, &header_comm); - ogg_stream_packetin (&os, &header_code); - - for (;;) - { - if (ogg_stream_flush (&os, &og) == 0) - break; - - output->write (og.header, og.header_len); - output->write (og.body, og.body_len); - } - - ok = true; - } - } - - ~OggWriter() - { - if (ok) - { - ogg_stream_clear (&os); - vorbis_block_clear (&vb); - vorbis_dsp_clear (&vd); - vorbis_comment_clear (&vc); - - vorbis_info_clear (&vi); - output->flush(); - } - else - { - vorbis_info_clear (&vi); - output = 0; // to stop the base class deleting this, as it needs to be returned - // to the caller of createWriter() - } - } - - //============================================================================== - bool write (const int** samplesToWrite, int numSamples) - { - if (! ok) - return false; - - if (numSamples > 0) - { - const double gain = 1.0 / 0x80000000u; - float** const vorbisBuffer = vorbis_analysis_buffer (&vd, numSamples); - - for (int i = numChannels; --i >= 0;) - { - float* const dst = vorbisBuffer[i]; - const int* const src = samplesToWrite [i]; - - if (src != 0 && dst != 0) - { - for (int j = 0; j < numSamples; ++j) - dst[j] = (float) (src[j] * gain); - } - } - } - - vorbis_analysis_wrote (&vd, numSamples); - - while (vorbis_analysis_blockout (&vd, &vb) == 1) - { - vorbis_analysis (&vb, 0); - vorbis_bitrate_addblock (&vb); - - while (vorbis_bitrate_flushpacket (&vd, &op)) - { - ogg_stream_packetin (&os, &op); - - for (;;) - { - if (ogg_stream_pageout (&os, &og) == 0) - break; - - output->write (og.header, og.header_len); - output->write (og.body, og.body_len); - - if (ogg_page_eos (&og)) - break; - } - } - } - - return true; - } - - juce_UseDebuggingNewOperator -}; - - -//============================================================================== -OggVorbisAudioFormat::OggVorbisAudioFormat() - : AudioFormat (oggFormatName, (const tchar**) oggExtensions) -{ -} - -OggVorbisAudioFormat::~OggVorbisAudioFormat() -{ -} - -const Array OggVorbisAudioFormat::getPossibleSampleRates() -{ - const int rates[] = { 22050, 32000, 44100, 48000, 0 }; - return Array (rates); -} - -const Array OggVorbisAudioFormat::getPossibleBitDepths() -{ - Array depths; - depths.add (32); - return depths; -} - -bool OggVorbisAudioFormat::canDoStereo() -{ - return true; -} - -bool OggVorbisAudioFormat::canDoMono() -{ - return true; -} - -AudioFormatReader* OggVorbisAudioFormat::createReaderFor (InputStream* in, - const bool deleteStreamIfOpeningFails) -{ - OggReader* r = new OggReader (in); - - if (r->sampleRate == 0) - { - if (! deleteStreamIfOpeningFails) - r->input = 0; - - deleteAndZero (r); - } - - return r; -} - -AudioFormatWriter* OggVorbisAudioFormat::createWriterFor (OutputStream* out, - double sampleRate, - unsigned int numChannels, - int bitsPerSample, - const StringPairArray& /*metadataValues*/, - int qualityOptionIndex) -{ - OggWriter* w = new OggWriter (out, - sampleRate, - numChannels, - bitsPerSample, - qualityOptionIndex); - - if (! w->ok) - deleteAndZero (w); - - return w; -} - -bool OggVorbisAudioFormat::isCompressed() -{ - return true; -} - -const StringArray OggVorbisAudioFormat::getQualityOptions() -{ - StringArray s; - s.add ("Low Quality"); - s.add ("Medium Quality"); - s.add ("High Quality"); - return s; -} - -int OggVorbisAudioFormat::estimateOggFileQuality (const File& source) -{ - FileInputStream* const in = source.createInputStream(); - - if (in != 0) - { - AudioFormatReader* const r = createReaderFor (in, true); - - if (r != 0) - { - const int64 numSamps = r->lengthInSamples; - delete r; - - const int64 fileNumSamps = source.getSize() / 4; - const double ratio = numSamps / (double) fileNumSamps; - - if (ratio > 12.0) - return 0; - else if (ratio > 6.0) - return 1; - else - return 2; - } - } - - return 1; -} - -END_JUCE_NAMESPACE - -#endif +/* + ============================================================================== + + This file is part of the JUCE library - "Jules' Utility Class Extensions" + Copyright 2004-7 by Raw Material Software ltd. + + ------------------------------------------------------------------------------ + + JUCE can be redistributed and/or modified 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. + + JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA + + ------------------------------------------------------------------------------ + + If you'd like to release a closed-source product which uses JUCE, commercial + licenses are also available: visit www.rawmaterialsoftware.com/juce for + more information. + + ============================================================================== +*/ + +#include "../../../../juce_Config.h" + +#if JUCE_USE_OGGVORBIS + +#include "../../../juce_core/basics/juce_StandardHeader.h" + +#if JUCE_MAC + #define __MACOSX__ 1 +#endif + + +namespace OggVorbisNamespace +{ +#include "oggvorbis/vorbisenc.h" +#include "oggvorbis/codec.h" +#include "oggvorbis/vorbisfile.h" + +#include "oggvorbis/bitwise.c" +#include "oggvorbis/framing.c" +#include "oggvorbis/libvorbis-1.1.2/lib/analysis.c" +#include "oggvorbis/libvorbis-1.1.2/lib/bitrate.c" +#include "oggvorbis/libvorbis-1.1.2/lib/block.c" +#include "oggvorbis/libvorbis-1.1.2/lib/codebook.c" +#include "oggvorbis/libvorbis-1.1.2/lib/envelope.c" +#include "oggvorbis/libvorbis-1.1.2/lib/floor0.c" +#include "oggvorbis/libvorbis-1.1.2/lib/floor1.c" +#include "oggvorbis/libvorbis-1.1.2/lib/info.c" +#include "oggvorbis/libvorbis-1.1.2/lib/lpc.c" +#include "oggvorbis/libvorbis-1.1.2/lib/lsp.c" +#include "oggvorbis/libvorbis-1.1.2/lib/mapping0.c" +#include "oggvorbis/libvorbis-1.1.2/lib/mdct.c" +#include "oggvorbis/libvorbis-1.1.2/lib/psy.c" +#include "oggvorbis/libvorbis-1.1.2/lib/registry.c" +#include "oggvorbis/libvorbis-1.1.2/lib/res0.c" +#include "oggvorbis/libvorbis-1.1.2/lib/sharedbook.c" +#include "oggvorbis/libvorbis-1.1.2/lib/smallft.c" +#include "oggvorbis/libvorbis-1.1.2/lib/synthesis.c" +#include "oggvorbis/libvorbis-1.1.2/lib/vorbisenc.c" +#include "oggvorbis/libvorbis-1.1.2/lib/vorbisfile.c" +#include "oggvorbis/libvorbis-1.1.2/lib/window.c" +} + +BEGIN_JUCE_NAMESPACE + +#include "juce_OggVorbisAudioFormat.h" +#include "../../application/juce_Application.h" +#include "../../../juce_core/basics/juce_Random.h" +#include "../../../juce_core/io/files/juce_FileInputStream.h" +#include "../../../juce_core/text/juce_LocalisedStrings.h" + +using namespace OggVorbisNamespace; + +//============================================================================== +#define oggFormatName TRANS("Ogg-Vorbis file") +static const tchar* const oggExtensions[] = { T(".ogg"), 0 }; + + +//============================================================================== +class OggReader : public AudioFormatReader +{ + OggVorbis_File ovFile; + ov_callbacks callbacks; + AudioSampleBuffer reservoir; + int reservoirStart, samplesInReservoir; + +public: + //============================================================================== + OggReader (InputStream* const inp) + : AudioFormatReader (inp, oggFormatName), + reservoir (2, 4096), + reservoirStart (0), + samplesInReservoir (0) + { + sampleRate = 0; + usesFloatingPointData = true; + + callbacks.read_func = &oggReadCallback; + callbacks.seek_func = &oggSeekCallback; + callbacks.close_func = &oggCloseCallback; + callbacks.tell_func = &oggTellCallback; + + const int err = ov_open_callbacks (input, &ovFile, 0, 0, callbacks); + + if (err == 0) + { + vorbis_info* info = ov_info (&ovFile, -1); + lengthInSamples = (uint32) ov_pcm_total (&ovFile, -1); + numChannels = info->channels; + bitsPerSample = 16; + sampleRate = info->rate; + + reservoir.setSize (numChannels, + (int) jmin (lengthInSamples, (int64) reservoir.getNumSamples())); + } + } + + ~OggReader() + { + ov_clear (&ovFile); + } + + //============================================================================== + bool read (int** destSamples, + int64 startSampleInFile, + int numSamples) + { + int startOffsetInDestBuffer = 0; + + if (startSampleInFile < 0) + { + const int silence = (int) jmin (-startSampleInFile, (int64) numSamples); + + int** destChan = destSamples; + + for (int i = 2; --i >= 0;) + { + if (*destChan != 0) + { + zeromem (*destChan, sizeof (int) * silence); + ++destChan; + } + } + + startOffsetInDestBuffer += silence; + numSamples -= silence; + } + + while (numSamples > 0) + { + const int numAvailable = reservoirStart + samplesInReservoir - startSampleInFile; + + if (startSampleInFile >= reservoirStart && numAvailable > 0) + { + // got a few samples overlapping, so use them before seeking.. + + const int numToUse = jmin (numSamples, numAvailable); + + for (unsigned int i = 0; i < numChannels; ++i) + { + if (destSamples[i] == 0) + break; + + memcpy (destSamples[i] + startOffsetInDestBuffer, + reservoir.getSampleData (jmin (i, reservoir.getNumChannels()), + (int) (startSampleInFile - reservoirStart)), + sizeof (float) * numToUse); + } + + startSampleInFile += numToUse; + numSamples -= numToUse; + startOffsetInDestBuffer += numToUse; + + if (numSamples == 0) + break; + } + + if (startSampleInFile < reservoirStart + || startSampleInFile + numSamples > reservoirStart + samplesInReservoir) + { + // buffer miss, so refill the reservoir + int bitStream = 0; + + reservoirStart = jmax (0, (int) startSampleInFile); + samplesInReservoir = reservoir.getNumSamples(); + + if (reservoirStart != (int) ov_pcm_tell (&ovFile)) + ov_pcm_seek (&ovFile, reservoirStart); + + int offset = 0; + int numToRead = samplesInReservoir; + + while (numToRead > 0) + { + float** dataIn = 0; + + const int samps = ov_read_float (&ovFile, &dataIn, numToRead, &bitStream); + if (samps == 0) + break; + + jassert (samps <= numToRead); + + for (int i = jmin (numChannels, reservoir.getNumChannels()); --i >= 0;) + { + memcpy (reservoir.getSampleData (i, offset), + dataIn[i], + sizeof (float) * samps); + } + + numToRead -= samps; + offset += samps; + } + + if (numToRead > 0) + reservoir.clear (offset, numToRead); + } + } + + return true; + } + + //============================================================================== + static size_t oggReadCallback (void* ptr, size_t size, size_t nmemb, void* datasource) + { + return (size_t) (((InputStream*) datasource)->read (ptr, (int) (size * nmemb)) / size); + } + + static int oggSeekCallback (void* datasource, ogg_int64_t offset, int whence) + { + InputStream* const in = (InputStream*) datasource; + + if (whence == SEEK_CUR) + offset += in->getPosition(); + else if (whence == SEEK_END) + offset += in->getTotalLength(); + + in->setPosition (offset); + return 0; + } + + static int oggCloseCallback (void*) + { + return 0; + } + + static long oggTellCallback (void* datasource) + { + return (long) ((InputStream*) datasource)->getPosition(); + } + + juce_UseDebuggingNewOperator +}; + +//============================================================================== +class OggWriter : public AudioFormatWriter +{ + ogg_stream_state os; + ogg_page og; + ogg_packet op; + vorbis_info vi; + vorbis_comment vc; + vorbis_dsp_state vd; + vorbis_block vb; + +public: + bool ok; + + //============================================================================== + OggWriter (OutputStream* const out, + const double sampleRate, + const int numChannels, + const int bitsPerSample, + const int qualityIndex) + : AudioFormatWriter (out, oggFormatName, + sampleRate, + numChannels, + bitsPerSample) + { + ok = false; + + vorbis_info_init (&vi); + + if (vorbis_encode_init_vbr (&vi, + numChannels, + (int) sampleRate, + jlimit (0.0f, 1.0f, qualityIndex * 0.5f)) == 0) + { + vorbis_comment_init (&vc); + + if (JUCEApplication::getInstance() != 0) + vorbis_comment_add_tag (&vc, "ENCODER", + (char*) (const char*) JUCEApplication::getInstance()->getApplicationName()); + + vorbis_analysis_init (&vd, &vi); + vorbis_block_init (&vd, &vb); + + ogg_stream_init (&os, Random::getSystemRandom().nextInt()); + + ogg_packet header; + ogg_packet header_comm; + ogg_packet header_code; + + vorbis_analysis_headerout (&vd, &vc, &header, &header_comm, &header_code); + + ogg_stream_packetin (&os, &header); + ogg_stream_packetin (&os, &header_comm); + ogg_stream_packetin (&os, &header_code); + + for (;;) + { + if (ogg_stream_flush (&os, &og) == 0) + break; + + output->write (og.header, og.header_len); + output->write (og.body, og.body_len); + } + + ok = true; + } + } + + ~OggWriter() + { + if (ok) + { + ogg_stream_clear (&os); + vorbis_block_clear (&vb); + vorbis_dsp_clear (&vd); + vorbis_comment_clear (&vc); + + vorbis_info_clear (&vi); + output->flush(); + } + else + { + vorbis_info_clear (&vi); + output = 0; // to stop the base class deleting this, as it needs to be returned + // to the caller of createWriter() + } + } + + //============================================================================== + bool write (const int** samplesToWrite, int numSamples) + { + if (! ok) + return false; + + if (numSamples > 0) + { + const double gain = 1.0 / 0x80000000u; + float** const vorbisBuffer = vorbis_analysis_buffer (&vd, numSamples); + + for (int i = numChannels; --i >= 0;) + { + float* const dst = vorbisBuffer[i]; + const int* const src = samplesToWrite [i]; + + if (src != 0 && dst != 0) + { + for (int j = 0; j < numSamples; ++j) + dst[j] = (float) (src[j] * gain); + } + } + } + + vorbis_analysis_wrote (&vd, numSamples); + + while (vorbis_analysis_blockout (&vd, &vb) == 1) + { + vorbis_analysis (&vb, 0); + vorbis_bitrate_addblock (&vb); + + while (vorbis_bitrate_flushpacket (&vd, &op)) + { + ogg_stream_packetin (&os, &op); + + for (;;) + { + if (ogg_stream_pageout (&os, &og) == 0) + break; + + output->write (og.header, og.header_len); + output->write (og.body, og.body_len); + + if (ogg_page_eos (&og)) + break; + } + } + } + + return true; + } + + juce_UseDebuggingNewOperator +}; + + +//============================================================================== +OggVorbisAudioFormat::OggVorbisAudioFormat() + : AudioFormat (oggFormatName, (const tchar**) oggExtensions) +{ +} + +OggVorbisAudioFormat::~OggVorbisAudioFormat() +{ +} + +const Array OggVorbisAudioFormat::getPossibleSampleRates() +{ + const int rates[] = { 22050, 32000, 44100, 48000, 0 }; + return Array (rates); +} + +const Array OggVorbisAudioFormat::getPossibleBitDepths() +{ + Array depths; + depths.add (32); + return depths; +} + +bool OggVorbisAudioFormat::canDoStereo() +{ + return true; +} + +bool OggVorbisAudioFormat::canDoMono() +{ + return true; +} + +AudioFormatReader* OggVorbisAudioFormat::createReaderFor (InputStream* in, + const bool deleteStreamIfOpeningFails) +{ + OggReader* r = new OggReader (in); + + if (r->sampleRate == 0) + { + if (! deleteStreamIfOpeningFails) + r->input = 0; + + deleteAndZero (r); + } + + return r; +} + +AudioFormatWriter* OggVorbisAudioFormat::createWriterFor (OutputStream* out, + double sampleRate, + unsigned int numChannels, + int bitsPerSample, + const StringPairArray& /*metadataValues*/, + int qualityOptionIndex) +{ + OggWriter* w = new OggWriter (out, + sampleRate, + numChannels, + bitsPerSample, + qualityOptionIndex); + + if (! w->ok) + deleteAndZero (w); + + return w; +} + +bool OggVorbisAudioFormat::isCompressed() +{ + return true; +} + +const StringArray OggVorbisAudioFormat::getQualityOptions() +{ + StringArray s; + s.add ("Low Quality"); + s.add ("Medium Quality"); + s.add ("High Quality"); + return s; +} + +int OggVorbisAudioFormat::estimateOggFileQuality (const File& source) +{ + FileInputStream* const in = source.createInputStream(); + + if (in != 0) + { + AudioFormatReader* const r = createReaderFor (in, true); + + if (r != 0) + { + const int64 numSamps = r->lengthInSamples; + delete r; + + const int64 fileNumSamps = source.getSize() / 4; + const double ratio = numSamps / (double) fileNumSamps; + + if (ratio > 12.0) + return 0; + else if (ratio > 6.0) + return 1; + else + return 2; + } + } + + return 1; +} + +END_JUCE_NAMESPACE + +#endif diff --git a/src/juce_appframework/audio/midi/juce_MidiMessage.cpp b/src/juce_appframework/audio/midi/juce_MidiMessage.cpp index 9cef12993e..912099fe1c 100644 --- a/src/juce_appframework/audio/midi/juce_MidiMessage.cpp +++ b/src/juce_appframework/audio/midi/juce_MidiMessage.cpp @@ -63,7 +63,6 @@ int MidiMessage::getMessageLengthFromFirstByte (const uint8 firstByte) throw() { // this method only works for valid starting bytes of a short midi message jassert (firstByte >= 0x80 - && firstByte != 0xff && firstByte != 0xf0 && firstByte != 0xf7); diff --git a/src/juce_appframework/audio/midi/juce_MidiMessageCollector.cpp b/src/juce_appframework/audio/midi/juce_MidiMessageCollector.cpp index 85c30b0c1f..9bbfff1886 100644 --- a/src/juce_appframework/audio/midi/juce_MidiMessageCollector.cpp +++ b/src/juce_appframework/audio/midi/juce_MidiMessageCollector.cpp @@ -110,7 +110,7 @@ void MidiMessageCollector::removeNextBlockOfMessages (MidiBuffer& destBuffer, { // if our list of events is longer than the buffer we're being // asked for, scale them down to squeeze them all in.. - const int maxBlockLengthToUse = numSamples << 3; + const int maxBlockLengthToUse = numSamples << 5; if (numSourceSamples > maxBlockLengthToUse) { @@ -150,7 +150,7 @@ void MidiMessageCollector::removeNextBlockOfMessages (MidiBuffer& destBuffer, void MidiMessageCollector::handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) { MidiMessage m (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity)); - m.setTimeStamp (Time::getMillisecondCounter() * 0.001); + m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001); addMessageToQueue (m); } @@ -158,7 +158,7 @@ void MidiMessageCollector::handleNoteOn (MidiKeyboardState*, int midiChannel, in void MidiMessageCollector::handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber) { MidiMessage m (MidiMessage::noteOff (midiChannel, midiNoteNumber)); - m.setTimeStamp (Time::getMillisecondCounter() * 0.001); + m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001); addMessageToQueue (m); } diff --git a/src/juce_appframework/gui/components/controls/juce_Label.cpp b/src/juce_appframework/gui/components/controls/juce_Label.cpp index eabde790b1..cad06e6f08 100644 --- a/src/juce_appframework/gui/components/controls/juce_Label.cpp +++ b/src/juce_appframework/gui/components/controls/juce_Label.cpp @@ -1,444 +1,456 @@ -/* - ============================================================================== - - This file is part of the JUCE library - "Jules' Utility Class Extensions" - Copyright 2004-7 by Raw Material Software ltd. - - ------------------------------------------------------------------------------ - - JUCE can be redistributed and/or modified 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. - - JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the - Free Software Foundation, Inc., 59 Temple Place, Suite 330, - Boston, MA 02111-1307 USA - - ------------------------------------------------------------------------------ - - If you'd like to release a closed-source product which uses JUCE, commercial - licenses are also available: visit www.rawmaterialsoftware.com/juce for - more information. - - ============================================================================== -*/ - -#include "../../../../juce_core/basics/juce_StandardHeader.h" - -BEGIN_JUCE_NAMESPACE - -#include "juce_Label.h" - - -//============================================================================== -Label::Label (const String& componentName, - const String& labelText) - : Component (componentName), - text (labelText), - font (15.0f), - justification (Justification::centredLeft), - editor (0), - listeners (2), - ownerComponent (0), - deletionWatcher (0), - editSingleClick (false), - editDoubleClick (false), - lossOfFocusDiscardsChanges (false) -{ - setColour (TextEditor::textColourId, Colours::black); - setColour (TextEditor::backgroundColourId, Colours::transparentBlack); - setColour (TextEditor::outlineColourId, Colours::transparentBlack); -} - -Label::~Label() -{ - if (ownerComponent != 0 && ! deletionWatcher->hasBeenDeleted()) - ownerComponent->removeComponentListener (this); - - deleteAndZero (deletionWatcher); - - if (editor != 0) - delete editor; -} - -//============================================================================== -void Label::setText (const String& newText, - const bool broadcastChangeMessage) -{ - hideEditor (true); - - if (text != newText) - { - text = newText; - - if (broadcastChangeMessage) - triggerAsyncUpdate(); - - repaint(); - - if (ownerComponent != 0 && ! deletionWatcher->hasBeenDeleted()) - componentMovedOrResized (*ownerComponent, true, true); - } -} - -const String Label::getText (const bool returnActiveEditorContents) const throw() -{ - return (returnActiveEditorContents && isBeingEdited()) - ? editor->getText() - : text; -} - -void Label::setFont (const Font& newFont) throw() -{ - font = newFont; - repaint(); -} - -const Font& Label::getFont() const throw() -{ - return font; -} - -void Label::setEditable (const bool editOnSingleClick, - const bool editOnDoubleClick, - const bool lossOfFocusDiscardsChanges_) throw() -{ - editSingleClick = editOnSingleClick; - editDoubleClick = editOnDoubleClick; - lossOfFocusDiscardsChanges = lossOfFocusDiscardsChanges_; - - setWantsKeyboardFocus (editOnSingleClick || editOnDoubleClick); - setFocusContainer (editOnSingleClick || editOnDoubleClick); -} - -void Label::setJustificationType (const Justification& justification_) throw() -{ - justification = justification_; - repaint(); -} - -//============================================================================== -void Label::attachToComponent (Component* owner, - const bool onLeft) -{ - if (ownerComponent != 0 && ! deletionWatcher->hasBeenDeleted()) - ownerComponent->removeComponentListener (this); - - deleteAndZero (deletionWatcher); - ownerComponent = owner; - - leftOfOwnerComp = onLeft; - - if (ownerComponent != 0) - { - deletionWatcher = new ComponentDeletionWatcher (owner); - - setVisible (owner->isVisible()); - ownerComponent->addComponentListener (this); - componentParentHierarchyChanged (*ownerComponent); - componentMovedOrResized (*ownerComponent, true, true); - } -} - -void Label::componentMovedOrResized (Component& component, - bool /*wasMoved*/, - bool /*wasResized*/) -{ - if (leftOfOwnerComp) - { - setSize (jmin (getFont().getStringWidth (text) + 8, component.getX()), - component.getHeight()); - - setTopRightPosition (component.getX(), component.getY()); - } - else - { - setSize (component.getWidth(), - 8 + roundFloatToInt (getFont().getHeight())); - - setTopLeftPosition (component.getX(), component.getY() - getHeight()); - } -} - -void Label::componentParentHierarchyChanged (Component& component) -{ - if (component.getParentComponent() != 0) - component.getParentComponent()->addChildComponent (this); -} - -void Label::componentVisibilityChanged (Component& component) -{ - setVisible (component.isVisible()); -} - -//============================================================================== -void Label::textWasEdited() -{ -} - -void Label::showEditor() -{ - if (editor == 0) - { - addAndMakeVisible (editor = createEditorComponent()); - editor->setText (getText()); - editor->addListener (this); - editor->grabKeyboardFocus(); - editor->setHighlightedRegion (0, text.length()); - editor->addListener (this); - - resized(); - repaint(); - - enterModalState(); - editor->grabKeyboardFocus(); - } -} - -bool Label::updateFromTextEditorContents() -{ - jassert (editor != 0); - const String newText (editor->getText()); - - if (text != newText) - { - text = newText; - - triggerAsyncUpdate(); - repaint(); - - if (ownerComponent != 0 && ! deletionWatcher->hasBeenDeleted()) - componentMovedOrResized (*ownerComponent, true, true); - - return true; - } - - return false; -} - -void Label::hideEditor (const bool discardCurrentEditorContents) -{ - if (editor != 0) - { - const bool changed = (! discardCurrentEditorContents) - && updateFromTextEditorContents(); - - deleteAndZero (editor); - repaint(); - - if (changed) - textWasEdited(); - - exitModalState (0); - } -} - -void Label::inputAttemptWhenModal() -{ - if (editor != 0) - { - if (lossOfFocusDiscardsChanges) - textEditorEscapeKeyPressed (*editor); - else - textEditorReturnKeyPressed (*editor); - } -} - -bool Label::isBeingEdited() const throw() -{ - return editor != 0; -} - -TextEditor* Label::createEditorComponent() -{ - TextEditor* const ed = new TextEditor (getName()); - ed->setFont (font); - - // copy these colours from our own settings.. - const int cols[] = { TextEditor::backgroundColourId, - TextEditor::textColourId, - TextEditor::highlightColourId, - TextEditor::highlightedTextColourId, - TextEditor::caretColourId, - TextEditor::outlineColourId, - TextEditor::focusedOutlineColourId, - TextEditor::shadowColourId }; - - for (int i = 0; i < numElementsInArray (cols); ++i) - ed->setColour (cols[i], findColour (cols[i])); - - return ed; -} - -//============================================================================== -void Label::paint (Graphics& g) -{ - g.fillAll (findColour (backgroundColourId)); - - if (editor == 0) - { - const float alpha = isEnabled() ? 1.0f : 0.5f; - - g.setColour (findColour (textColourId).withMultipliedAlpha (alpha)); - g.setFont (font); - g.drawFittedText (text, - 3, 1, getWidth() - 6, getHeight() - 2, - justification, - jmax (1, (int) (getHeight() / font.getHeight()))); - - g.setColour (findColour (outlineColourId).withMultipliedAlpha (alpha)); - g.drawRect (0, 0, getWidth(), getHeight()); - } - else if (isEnabled()) - { - g.setColour (editor->findColour (TextEditor::backgroundColourId) - .overlaidWith (findColour (outlineColourId))); - - g.drawRect (0, 0, getWidth(), getHeight()); - } -} - -void Label::mouseUp (const MouseEvent& e) -{ - if (editSingleClick - && e.mouseWasClicked() - && contains (e.x, e.y) - && ! e.mods.isPopupMenu()) - { - showEditor(); - } -} - -void Label::mouseDoubleClick (const MouseEvent& e) -{ - if (editDoubleClick && ! e.mods.isPopupMenu()) - showEditor(); -} - -void Label::resized() -{ - if (editor != 0) - editor->setBoundsInset (BorderSize (0)); -} - -void Label::focusGained (FocusChangeType cause) -{ - if (editSingleClick && cause == focusChangedByTabKey) - showEditor(); -} - -void Label::enablementChanged() -{ - repaint(); -} - -void Label::colourChanged() -{ - repaint(); -} - -//============================================================================== -// We'll use a custom focus traverser here to make sure focus goes from the -// text editor to another component rather than back to the label itself. -class LabelKeyboardFocusTraverser : public KeyboardFocusTraverser -{ -public: - LabelKeyboardFocusTraverser() {} - - Component* getNextComponent (Component* current) - { - return KeyboardFocusTraverser::getNextComponent (dynamic_cast (current) != 0 - ? current->getParentComponent() : current); - } - - Component* getPreviousComponent (Component* current) - { - return KeyboardFocusTraverser::getPreviousComponent (dynamic_cast (current) != 0 - ? current->getParentComponent() : current); - } -}; - -KeyboardFocusTraverser* Label::createFocusTraverser() -{ - return new LabelKeyboardFocusTraverser(); -} - -//============================================================================== -void Label::addListener (LabelListener* const listener) throw() -{ - jassert (listener != 0); - if (listener != 0) - listeners.add (listener); -} - -void Label::removeListener (LabelListener* const listener) throw() -{ - listeners.removeValue (listener); -} - -void Label::handleAsyncUpdate() -{ - for (int i = listeners.size(); --i >= 0;) - { - ((LabelListener*) listeners.getUnchecked (i))->labelTextChanged (this); - i = jmin (i, listeners.size()); - } -} - -//============================================================================== -void Label::textEditorTextChanged (TextEditor& ed) -{ - if (editor != 0) - { - jassert (&ed == editor); - - if (! (hasKeyboardFocus (true) || isCurrentlyBlockedByAnotherModalComponent())) - { - if (lossOfFocusDiscardsChanges) - textEditorEscapeKeyPressed (ed); - else - textEditorReturnKeyPressed (ed); - } - } -} - -void Label::textEditorReturnKeyPressed (TextEditor& ed) -{ - if (editor != 0) - { - jassert (&ed == editor); - (void) ed; - - const bool changed = updateFromTextEditorContents(); - hideEditor (true); - - if (changed) - textWasEdited(); - } -} - -void Label::textEditorEscapeKeyPressed (TextEditor& ed) -{ - if (editor != 0) - { - jassert (&ed == editor); - (void) ed; - - editor->setText (text, false); - hideEditor (true); - } -} - -void Label::textEditorFocusLost (TextEditor& ed) -{ - textEditorTextChanged (ed); -} - - -END_JUCE_NAMESPACE +/* + ============================================================================== + + This file is part of the JUCE library - "Jules' Utility Class Extensions" + Copyright 2004-7 by Raw Material Software ltd. + + ------------------------------------------------------------------------------ + + JUCE can be redistributed and/or modified 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. + + JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA + + ------------------------------------------------------------------------------ + + If you'd like to release a closed-source product which uses JUCE, commercial + licenses are also available: visit www.rawmaterialsoftware.com/juce for + more information. + + ============================================================================== +*/ + +#include "../../../../juce_core/basics/juce_StandardHeader.h" + +BEGIN_JUCE_NAMESPACE + +#include "juce_Label.h" + + +//============================================================================== +Label::Label (const String& componentName, + const String& labelText) + : Component (componentName), + text (labelText), + font (15.0f), + justification (Justification::centredLeft), + editor (0), + listeners (2), + ownerComponent (0), + deletionWatcher (0), + horizontalBorderSize (3), + verticalBorderSize (1), + editSingleClick (false), + editDoubleClick (false), + lossOfFocusDiscardsChanges (false) +{ + setColour (TextEditor::textColourId, Colours::black); + setColour (TextEditor::backgroundColourId, Colours::transparentBlack); + setColour (TextEditor::outlineColourId, Colours::transparentBlack); +} + +Label::~Label() +{ + if (ownerComponent != 0 && ! deletionWatcher->hasBeenDeleted()) + ownerComponent->removeComponentListener (this); + + deleteAndZero (deletionWatcher); + + if (editor != 0) + delete editor; +} + +//============================================================================== +void Label::setText (const String& newText, + const bool broadcastChangeMessage) +{ + hideEditor (true); + + if (text != newText) + { + text = newText; + + if (broadcastChangeMessage) + triggerAsyncUpdate(); + + repaint(); + + if (ownerComponent != 0 && ! deletionWatcher->hasBeenDeleted()) + componentMovedOrResized (*ownerComponent, true, true); + } +} + +const String Label::getText (const bool returnActiveEditorContents) const throw() +{ + return (returnActiveEditorContents && isBeingEdited()) + ? editor->getText() + : text; +} + +void Label::setFont (const Font& newFont) throw() +{ + font = newFont; + repaint(); +} + +const Font& Label::getFont() const throw() +{ + return font; +} + +void Label::setEditable (const bool editOnSingleClick, + const bool editOnDoubleClick, + const bool lossOfFocusDiscardsChanges_) throw() +{ + editSingleClick = editOnSingleClick; + editDoubleClick = editOnDoubleClick; + lossOfFocusDiscardsChanges = lossOfFocusDiscardsChanges_; + + setWantsKeyboardFocus (editOnSingleClick || editOnDoubleClick); + setFocusContainer (editOnSingleClick || editOnDoubleClick); +} + +void Label::setJustificationType (const Justification& justification_) throw() +{ + justification = justification_; + repaint(); +} + +void Label::setBorderSize (int h, int v) +{ + horizontalBorderSize = h; + verticalBorderSize = v; + repaint(); +} + +//============================================================================== +void Label::attachToComponent (Component* owner, + const bool onLeft) +{ + if (ownerComponent != 0 && ! deletionWatcher->hasBeenDeleted()) + ownerComponent->removeComponentListener (this); + + deleteAndZero (deletionWatcher); + ownerComponent = owner; + + leftOfOwnerComp = onLeft; + + if (ownerComponent != 0) + { + deletionWatcher = new ComponentDeletionWatcher (owner); + + setVisible (owner->isVisible()); + ownerComponent->addComponentListener (this); + componentParentHierarchyChanged (*ownerComponent); + componentMovedOrResized (*ownerComponent, true, true); + } +} + +void Label::componentMovedOrResized (Component& component, + bool /*wasMoved*/, + bool /*wasResized*/) +{ + if (leftOfOwnerComp) + { + setSize (jmin (getFont().getStringWidth (text) + 8, component.getX()), + component.getHeight()); + + setTopRightPosition (component.getX(), component.getY()); + } + else + { + setSize (component.getWidth(), + 8 + roundFloatToInt (getFont().getHeight())); + + setTopLeftPosition (component.getX(), component.getY() - getHeight()); + } +} + +void Label::componentParentHierarchyChanged (Component& component) +{ + if (component.getParentComponent() != 0) + component.getParentComponent()->addChildComponent (this); +} + +void Label::componentVisibilityChanged (Component& component) +{ + setVisible (component.isVisible()); +} + +//============================================================================== +void Label::textWasEdited() +{ +} + +void Label::showEditor() +{ + if (editor == 0) + { + addAndMakeVisible (editor = createEditorComponent()); + editor->setText (getText()); + editor->addListener (this); + editor->grabKeyboardFocus(); + editor->setHighlightedRegion (0, text.length()); + editor->addListener (this); + + resized(); + repaint(); + + enterModalState(); + editor->grabKeyboardFocus(); + } +} + +bool Label::updateFromTextEditorContents() +{ + jassert (editor != 0); + const String newText (editor->getText()); + + if (text != newText) + { + text = newText; + + triggerAsyncUpdate(); + repaint(); + + if (ownerComponent != 0 && ! deletionWatcher->hasBeenDeleted()) + componentMovedOrResized (*ownerComponent, true, true); + + return true; + } + + return false; +} + +void Label::hideEditor (const bool discardCurrentEditorContents) +{ + if (editor != 0) + { + const bool changed = (! discardCurrentEditorContents) + && updateFromTextEditorContents(); + + deleteAndZero (editor); + repaint(); + + if (changed) + textWasEdited(); + + exitModalState (0); + } +} + +void Label::inputAttemptWhenModal() +{ + if (editor != 0) + { + if (lossOfFocusDiscardsChanges) + textEditorEscapeKeyPressed (*editor); + else + textEditorReturnKeyPressed (*editor); + } +} + +bool Label::isBeingEdited() const throw() +{ + return editor != 0; +} + +TextEditor* Label::createEditorComponent() +{ + TextEditor* const ed = new TextEditor (getName()); + ed->setFont (font); + + // copy these colours from our own settings.. + const int cols[] = { TextEditor::backgroundColourId, + TextEditor::textColourId, + TextEditor::highlightColourId, + TextEditor::highlightedTextColourId, + TextEditor::caretColourId, + TextEditor::outlineColourId, + TextEditor::focusedOutlineColourId, + TextEditor::shadowColourId }; + + for (int i = 0; i < numElementsInArray (cols); ++i) + ed->setColour (cols[i], findColour (cols[i])); + + return ed; +} + +//============================================================================== +void Label::paint (Graphics& g) +{ + g.fillAll (findColour (backgroundColourId)); + + if (editor == 0) + { + const float alpha = isEnabled() ? 1.0f : 0.5f; + + g.setColour (findColour (textColourId).withMultipliedAlpha (alpha)); + g.setFont (font); + g.drawFittedText (text, + horizontalBorderSize, + verticalBorderSize, + getWidth() - 2 * horizontalBorderSize, + getHeight() - 2 * verticalBorderSize, + justification, + jmax (1, (int) (getHeight() / font.getHeight()))); + + g.setColour (findColour (outlineColourId).withMultipliedAlpha (alpha)); + g.drawRect (0, 0, getWidth(), getHeight()); + } + else if (isEnabled()) + { + g.setColour (editor->findColour (TextEditor::backgroundColourId) + .overlaidWith (findColour (outlineColourId))); + + g.drawRect (0, 0, getWidth(), getHeight()); + } +} + +void Label::mouseUp (const MouseEvent& e) +{ + if (editSingleClick + && e.mouseWasClicked() + && contains (e.x, e.y) + && ! e.mods.isPopupMenu()) + { + showEditor(); + } +} + +void Label::mouseDoubleClick (const MouseEvent& e) +{ + if (editDoubleClick && ! e.mods.isPopupMenu()) + showEditor(); +} + +void Label::resized() +{ + if (editor != 0) + editor->setBoundsInset (BorderSize (0)); +} + +void Label::focusGained (FocusChangeType cause) +{ + if (editSingleClick && cause == focusChangedByTabKey) + showEditor(); +} + +void Label::enablementChanged() +{ + repaint(); +} + +void Label::colourChanged() +{ + repaint(); +} + +//============================================================================== +// We'll use a custom focus traverser here to make sure focus goes from the +// text editor to another component rather than back to the label itself. +class LabelKeyboardFocusTraverser : public KeyboardFocusTraverser +{ +public: + LabelKeyboardFocusTraverser() {} + + Component* getNextComponent (Component* current) + { + return KeyboardFocusTraverser::getNextComponent (dynamic_cast (current) != 0 + ? current->getParentComponent() : current); + } + + Component* getPreviousComponent (Component* current) + { + return KeyboardFocusTraverser::getPreviousComponent (dynamic_cast (current) != 0 + ? current->getParentComponent() : current); + } +}; + +KeyboardFocusTraverser* Label::createFocusTraverser() +{ + return new LabelKeyboardFocusTraverser(); +} + +//============================================================================== +void Label::addListener (LabelListener* const listener) throw() +{ + jassert (listener != 0); + if (listener != 0) + listeners.add (listener); +} + +void Label::removeListener (LabelListener* const listener) throw() +{ + listeners.removeValue (listener); +} + +void Label::handleAsyncUpdate() +{ + for (int i = listeners.size(); --i >= 0;) + { + ((LabelListener*) listeners.getUnchecked (i))->labelTextChanged (this); + i = jmin (i, listeners.size()); + } +} + +//============================================================================== +void Label::textEditorTextChanged (TextEditor& ed) +{ + if (editor != 0) + { + jassert (&ed == editor); + + if (! (hasKeyboardFocus (true) || isCurrentlyBlockedByAnotherModalComponent())) + { + if (lossOfFocusDiscardsChanges) + textEditorEscapeKeyPressed (ed); + else + textEditorReturnKeyPressed (ed); + } + } +} + +void Label::textEditorReturnKeyPressed (TextEditor& ed) +{ + if (editor != 0) + { + jassert (&ed == editor); + (void) ed; + + const bool changed = updateFromTextEditorContents(); + hideEditor (true); + + if (changed) + textWasEdited(); + } +} + +void Label::textEditorEscapeKeyPressed (TextEditor& ed) +{ + if (editor != 0) + { + jassert (&ed == editor); + (void) ed; + + editor->setText (text, false); + hideEditor (true); + } +} + +void Label::textEditorFocusLost (TextEditor& ed) +{ + textEditorTextChanged (ed); +} + + +END_JUCE_NAMESPACE diff --git a/src/juce_appframework/gui/components/controls/juce_Label.h b/src/juce_appframework/gui/components/controls/juce_Label.h index 09db081cb2..2b740fe852 100644 --- a/src/juce_appframework/gui/components/controls/juce_Label.h +++ b/src/juce_appframework/gui/components/controls/juce_Label.h @@ -148,6 +148,12 @@ public: /** Returns the type of justification, as set in setJustificationType(). */ const Justification getJustificationType() const throw() { return justification; } + /** Changes the gap that is left between the edge of the component and the text. + By default there's a small gap left at the sides of the component to allow for + the drawing of the border, but you can change this if necessary. + */ + void setBorderSize (int horizontalBorder, int verticalBorder); + /** Makes this label "stick to" another component. This will cause the label to follow another component around, staying @@ -294,7 +300,7 @@ private: SortedSet listeners; Component* ownerComponent; ComponentDeletionWatcher* deletionWatcher; - + int horizontalBorderSize, verticalBorderSize; bool editSingleClick : 1; bool editDoubleClick : 1; bool lossOfFocusDiscardsChanges : 1; diff --git a/src/juce_appframework/gui/components/controls/juce_Slider.cpp b/src/juce_appframework/gui/components/controls/juce_Slider.cpp index b5ac6b3895..5d0676f6ca 100644 --- a/src/juce_appframework/gui/components/controls/juce_Slider.cpp +++ b/src/juce_appframework/gui/components/controls/juce_Slider.cpp @@ -1,1366 +1,1368 @@ -/* - ============================================================================== - - This file is part of the JUCE library - "Jules' Utility Class Extensions" - Copyright 2004-7 by Raw Material Software ltd. - - ------------------------------------------------------------------------------ - - JUCE can be redistributed and/or modified 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. - - JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the - Free Software Foundation, Inc., 59 Temple Place, Suite 330, - Boston, MA 02111-1307 USA - - ------------------------------------------------------------------------------ - - If you'd like to release a closed-source product which uses JUCE, commercial - licenses are also available: visit www.rawmaterialsoftware.com/juce for - more information. - - ============================================================================== -*/ - -#include "../../../../juce_core/basics/juce_StandardHeader.h" - -BEGIN_JUCE_NAMESPACE - -#include "juce_Slider.h" -#include "../lookandfeel/juce_LookAndFeel.h" -#include "../menus/juce_PopupMenu.h" -#include "../juce_Desktop.h" -#include "../special/juce_BubbleComponent.h" -#include "../../../../juce_core/text/juce_LocalisedStrings.h" - - -//============================================================================== -class SliderPopupDisplayComponent : public BubbleComponent -{ -public: - //============================================================================== - SliderPopupDisplayComponent (Slider* const owner_) - : owner (owner_), - font (15.0f, Font::bold) - { - setAlwaysOnTop (true); - } - - ~SliderPopupDisplayComponent() - { - } - - void paintContent (Graphics& g, int w, int h) - { - g.setFont (font); - g.setColour (Colours::black); - - g.drawFittedText (text, 0, 0, w, h, Justification::centred, 1); - } - - void getContentSize (int& w, int& h) - { - w = font.getStringWidth (text) + 18; - h = (int) (font.getHeight() * 1.6f); - } - - void updatePosition (const String& newText) - { - if (text != newText) - { - text = newText; - repaint(); - } - - BubbleComponent::setPosition (owner); - } - - //============================================================================== - juce_UseDebuggingNewOperator - -private: - Slider* owner; - Font font; - String text; - - SliderPopupDisplayComponent (const SliderPopupDisplayComponent&); - const SliderPopupDisplayComponent& operator= (const SliderPopupDisplayComponent&); -}; - -//============================================================================== -Slider::Slider (const String& name) - : Component (name), - listeners (2), - currentValue (0.0), - valueMin (0.0), - valueMax (0.0), - minimum (0), - maximum (10), - interval (0), - skewFactor (1.0), - velocityModeSensitivity (1.0), - velocityModeOffset (0.0), - velocityModeThreshold (1), - rotaryStart (float_Pi * 1.2f), - rotaryEnd (float_Pi * 2.8f), - numDecimalPlaces (7), - sliderRegionStart (0), - sliderRegionSize (1), - sliderBeingDragged (-1), - pixelsForFullDragExtent (250), - style (LinearHorizontal), - textBoxPos (TextBoxLeft), - textBoxWidth (80), - textBoxHeight (20), - incDecButtonMode (incDecButtonsNotDraggable), - editableText (true), - doubleClickToValue (false), - isVelocityBased (false), - userKeyOverridesVelocity (true), - rotaryStop (true), - incDecButtonsSideBySide (false), - sendChangeOnlyOnRelease (false), - popupDisplayEnabled (false), - menuEnabled (false), - menuShown (false), - scrollWheelEnabled (true), - snapsToMousePos (true), - valueBox (0), - incButton (0), - decButton (0), - popupDisplay (0), - parentForPopupDisplay (0) -{ - setWantsKeyboardFocus (false); - setRepaintsOnMouseActivity (true); - - lookAndFeelChanged(); - updateText(); -} - -Slider::~Slider() -{ - deleteAndZero (popupDisplay); - deleteAllChildren(); -} - - -//============================================================================== -void Slider::handleAsyncUpdate() -{ - cancelPendingUpdate(); - - for (int i = listeners.size(); --i >= 0;) - { - ((SliderListener*) listeners.getUnchecked (i))->sliderValueChanged (this); - i = jmin (i, listeners.size()); - } -} - -void Slider::sendDragStart() -{ - startedDragging(); - - for (int i = listeners.size(); --i >= 0;) - { - ((SliderListener*) listeners.getUnchecked (i))->sliderDragStarted (this); - i = jmin (i, listeners.size()); - } -} - -void Slider::sendDragEnd() -{ - stoppedDragging(); - - sliderBeingDragged = -1; - - for (int i = listeners.size(); --i >= 0;) - { - ((SliderListener*) listeners.getUnchecked (i))->sliderDragEnded (this); - i = jmin (i, listeners.size()); - } -} - -void Slider::addListener (SliderListener* const listener) throw() -{ - jassert (listener != 0); - if (listener != 0) - listeners.add (listener); -} - -void Slider::removeListener (SliderListener* const listener) throw() -{ - listeners.removeValue (listener); -} - -//============================================================================== -void Slider::setSliderStyle (const SliderStyle newStyle) -{ - if (style != newStyle) - { - style = newStyle; - repaint(); - lookAndFeelChanged(); - } -} - -void Slider::setRotaryParameters (const float startAngleRadians, - const float endAngleRadians, - const bool stopAtEnd) -{ - // make sure the values are sensible.. - jassert (rotaryStart >= 0 && rotaryEnd >= 0); - jassert (rotaryStart < float_Pi * 4.0f && rotaryEnd < float_Pi * 4.0f); - jassert (rotaryStart < rotaryEnd); - - rotaryStart = startAngleRadians; - rotaryEnd = endAngleRadians; - rotaryStop = stopAtEnd; -} - -void Slider::setVelocityBasedMode (const bool velBased) throw() -{ - isVelocityBased = velBased; -} - -void Slider::setVelocityModeParameters (const double sensitivity, - const int threshold, - const double offset, - const bool userCanPressKeyToSwapMode) throw() -{ - jassert (threshold >= 0); - jassert (sensitivity > 0); - jassert (offset >= 0); - - velocityModeSensitivity = sensitivity; - velocityModeOffset = offset; - velocityModeThreshold = threshold; - userKeyOverridesVelocity = userCanPressKeyToSwapMode; -} - -void Slider::setSkewFactor (const double factor) throw() -{ - skewFactor = factor; -} - -void Slider::setSkewFactorFromMidPoint (const double sliderValueToShowAtMidPoint) throw() -{ - if (maximum > minimum) - skewFactor = log (0.5) / log ((sliderValueToShowAtMidPoint - minimum) - / (maximum - minimum)); -} - -void Slider::setMouseDragSensitivity (const int distanceForFullScaleDrag) -{ - jassert (distanceForFullScaleDrag > 0); - - pixelsForFullDragExtent = distanceForFullScaleDrag; -} - -void Slider::setIncDecButtonsMode (const IncDecButtonMode mode) -{ - if (incDecButtonMode != mode) - { - incDecButtonMode = mode; - lookAndFeelChanged(); - } -} - -void Slider::setTextBoxStyle (const TextEntryBoxPosition newPosition, - const bool isReadOnly, - const int textEntryBoxWidth, - const int textEntryBoxHeight) -{ - textBoxPos = newPosition; - editableText = ! isReadOnly; - textBoxWidth = textEntryBoxWidth; - textBoxHeight = textEntryBoxHeight; - - repaint(); - lookAndFeelChanged(); -} - -void Slider::setTextBoxIsEditable (const bool shouldBeEditable) throw() -{ - editableText = shouldBeEditable; - - if (valueBox != 0) - valueBox->setEditable (shouldBeEditable && isEnabled()); -} - -void Slider::showTextBox() -{ - jassert (editableText); // this should probably be avoided in read-only sliders. - - if (valueBox != 0) - valueBox->showEditor(); -} - -void Slider::hideTextBox (const bool discardCurrentEditorContents) -{ - if (valueBox != 0) - { - valueBox->hideEditor (discardCurrentEditorContents); - - if (discardCurrentEditorContents) - updateText(); - } -} - -void Slider::setChangeNotificationOnlyOnRelease (const bool onlyNotifyOnRelease) throw() -{ - sendChangeOnlyOnRelease = onlyNotifyOnRelease; -} - -void Slider::setSliderSnapsToMousePosition (const bool shouldSnapToMouse) throw() -{ - snapsToMousePos = shouldSnapToMouse; -} - -void Slider::setPopupDisplayEnabled (const bool enabled, - Component* const parentComponentToUse) throw() -{ - popupDisplayEnabled = enabled; - parentForPopupDisplay = parentComponentToUse; -} - -//============================================================================== -void Slider::colourChanged() -{ - lookAndFeelChanged(); -} - -void Slider::lookAndFeelChanged() -{ - const String previousTextBoxContent (valueBox != 0 ? valueBox->getText() - : getTextFromValue (currentValue)); - - deleteAllChildren(); - valueBox = 0; - - LookAndFeel& lf = getLookAndFeel(); - - if (textBoxPos != NoTextBox) - { - addAndMakeVisible (valueBox = getLookAndFeel().createSliderTextBox (*this)); - - valueBox->setWantsKeyboardFocus (false); - valueBox->setText (previousTextBoxContent, false); - - valueBox->setEditable (editableText && isEnabled()); - valueBox->addListener (this); - - if (style == LinearBar) - valueBox->addMouseListener (this, false); - } - - if (style == IncDecButtons) - { - addAndMakeVisible (incButton = lf.createSliderButton (true)); - incButton->addButtonListener (this); - - addAndMakeVisible (decButton = lf.createSliderButton (false)); - decButton->addButtonListener (this); - - if (incDecButtonMode != incDecButtonsNotDraggable) - { - incButton->addMouseListener (this, false); - decButton->addMouseListener (this, false); - } - else - { - incButton->setRepeatSpeed (300, 100, 20); - incButton->addMouseListener (decButton, false); - - decButton->setRepeatSpeed (300, 100, 20); - decButton->addMouseListener (incButton, false); - } - } - - setComponentEffect (lf.getSliderEffect()); - - resized(); - repaint(); -} - -//============================================================================== -void Slider::setRange (const double newMin, - const double newMax, - const double newInt) -{ - if (minimum != newMin - || maximum != newMax - || interval != newInt) - { - minimum = newMin; - maximum = newMax; - interval = newInt; - - // figure out the number of DPs needed to display all values at this - // interval setting. - numDecimalPlaces = 7; - - if (newInt != 0) - { - int v = abs ((int) (newInt * 10000000)); - - while ((v % 10) == 0) - { - --numDecimalPlaces; - v /= 10; - } - } - - // keep the current values inside the new range.. - if (style != TwoValueHorizontal && style != TwoValueVertical) - { - setValue (currentValue, false, false); - } - else - { - setMinValue (getMinValue(), false, false); - setMaxValue (getMaxValue(), false, false); - } - - updateText(); - } -} - -void Slider::triggerChangeMessage (const bool synchronous) -{ - if (synchronous) - handleAsyncUpdate(); - else - triggerAsyncUpdate(); - - valueChanged(); -} - -double Slider::getValue() const throw() -{ - // for a two-value style slider, you should use the getMinValue() and getMaxValue() - // methods to get the two values. - jassert (style != TwoValueHorizontal && style != TwoValueVertical); - - return currentValue; -} - -void Slider::setValue (double newValue, - const bool sendUpdateMessage, - const bool sendMessageSynchronously) -{ - // for a two-value style slider, you should use the setMinValue() and setMaxValue() - // methods to set the two values. - jassert (style != TwoValueHorizontal && style != TwoValueVertical); - - newValue = constrainedValue (newValue); - - if (style == ThreeValueHorizontal || style == ThreeValueVertical) - { - jassert (valueMin <= valueMax); - newValue = jlimit (valueMin, valueMax, newValue); - } - - if (currentValue != newValue) - { - if (valueBox != 0) - valueBox->hideEditor (true); - - currentValue = newValue; - updateText(); - repaint(); - - if (popupDisplay != 0) - { - ((SliderPopupDisplayComponent*) popupDisplay)->updatePosition (getTextFromValue (currentValue)); - popupDisplay->repaint(); - } - - if (sendUpdateMessage) - triggerChangeMessage (sendMessageSynchronously); - } -} - -double Slider::getMinValue() const throw() -{ - // The minimum value only applies to sliders that are in two- or three-value mode. - jassert (style == TwoValueHorizontal || style == TwoValueVertical - || style == ThreeValueHorizontal || style == ThreeValueVertical); - - return valueMin; -} - -double Slider::getMaxValue() const throw() -{ - // The maximum value only applies to sliders that are in two- or three-value mode. - jassert (style == TwoValueHorizontal || style == TwoValueVertical - || style == ThreeValueHorizontal || style == ThreeValueVertical); - - return valueMax; -} - -void Slider::setMinValue (double newValue, const bool sendUpdateMessage, const bool sendMessageSynchronously) -{ - // The minimum value only applies to sliders that are in two- or three-value mode. - jassert (style == TwoValueHorizontal || style == TwoValueVertical - || style == ThreeValueHorizontal || style == ThreeValueVertical); - - newValue = constrainedValue (newValue); - - if (style == TwoValueHorizontal || style == TwoValueVertical) - newValue = jmin (valueMax, newValue); - else - newValue = jmin (currentValue, newValue); - - if (valueMin != newValue) - { - valueMin = newValue; - repaint(); - - if (popupDisplay != 0) - { - ((SliderPopupDisplayComponent*) popupDisplay)->updatePosition (getTextFromValue (valueMin)); - popupDisplay->repaint(); - } - - if (sendUpdateMessage) - triggerChangeMessage (sendMessageSynchronously); - } -} - -void Slider::setMaxValue (double newValue, const bool sendUpdateMessage, const bool sendMessageSynchronously) -{ - // The maximum value only applies to sliders that are in two- or three-value mode. - jassert (style == TwoValueHorizontal || style == TwoValueVertical - || style == ThreeValueHorizontal || style == ThreeValueVertical); - - newValue = constrainedValue (newValue); - - if (style == TwoValueHorizontal || style == TwoValueVertical) - newValue = jmax (valueMin, newValue); - else - newValue = jmax (currentValue, newValue); - - if (valueMax != newValue) - { - valueMax = newValue; - repaint(); - - if (popupDisplay != 0) - { - ((SliderPopupDisplayComponent*) popupDisplay)->updatePosition (getTextFromValue (valueMax)); - popupDisplay->repaint(); - } - - if (sendUpdateMessage) - triggerChangeMessage (sendMessageSynchronously); - } -} - -void Slider::setDoubleClickReturnValue (const bool isDoubleClickEnabled, - const double valueToSetOnDoubleClick) throw() -{ - doubleClickToValue = isDoubleClickEnabled; - doubleClickReturnValue = valueToSetOnDoubleClick; -} - -double Slider::getDoubleClickReturnValue (bool& isEnabled_) const throw() -{ - isEnabled_ = doubleClickToValue; - return doubleClickReturnValue; -} - -void Slider::updateText() -{ - if (valueBox != 0) - valueBox->setText (getTextFromValue (currentValue), false); -} - -void Slider::setTextValueSuffix (const String& suffix) -{ - if (textSuffix != suffix) - { - textSuffix = suffix; - updateText(); - } -} - -const String Slider::getTextFromValue (double v) -{ - if (numDecimalPlaces > 0) - return String (v, numDecimalPlaces) + textSuffix; - else - return String (roundDoubleToInt (v)) + textSuffix; -} - -double Slider::getValueFromText (const String& text) -{ - String t (text.trimStart()); - - if (t.endsWith (textSuffix)) - t = t.substring (0, t.length() - textSuffix.length()); - - while (t.startsWithChar (T('+'))) - t = t.substring (1).trimStart(); - - return t.initialSectionContainingOnly (T("0123456789.,-")) - .getDoubleValue(); -} - -double Slider::proportionOfLengthToValue (double proportion) -{ - if (skewFactor != 1.0 && proportion > 0.0) - proportion = exp (log (proportion) / skewFactor); - - return minimum + (maximum - minimum) * proportion; -} - -double Slider::valueToProportionOfLength (double value) -{ - const double n = (value - minimum) / (maximum - minimum); - - return skewFactor == 1.0 ? n : pow (n, skewFactor); -} - -double Slider::snapValue (double attemptedValue, const bool) -{ - return attemptedValue; -} - -//============================================================================== -void Slider::startedDragging() -{ -} - -void Slider::stoppedDragging() -{ -} - -void Slider::valueChanged() -{ -} - -//============================================================================== -void Slider::enablementChanged() -{ - repaint(); -} - -void Slider::setPopupMenuEnabled (const bool menuEnabled_) throw() -{ - menuEnabled = menuEnabled_; -} - -void Slider::setScrollWheelEnabled (const bool enabled) throw() -{ - scrollWheelEnabled = enabled; -} - -//============================================================================== -void Slider::labelTextChanged (Label* label) -{ - const double newValue = snapValue (getValueFromText (label->getText()), false); - - if (getValue() != newValue) - { - sendDragStart(); - setValue (newValue, true, true); - sendDragEnd(); - } - - updateText(); // force a clean-up of the text, needed in case setValue() hasn't done this. -} - -void Slider::buttonClicked (Button* button) -{ - if (style == IncDecButtons) - { - sendDragStart(); - - if (button == incButton) - setValue (snapValue (getValue() + interval, false), true, true); - else if (button == decButton) - setValue (snapValue (getValue() - interval, false), true, true); - - sendDragEnd(); - } -} - -//============================================================================== -double Slider::constrainedValue (double value) const throw() -{ - if (interval > 0) - value = minimum + interval * floor ((value - minimum) / interval + 0.5); - - if (value <= minimum || maximum <= minimum) - value = minimum; - else if (value >= maximum) - value = maximum; - - return value; -} - -float Slider::getLinearSliderPos (const double value) -{ - double sliderPosProportional; - - if (maximum > minimum) - { - if (value < minimum) - { - sliderPosProportional = 0.0; - } - else if (value > maximum) - { - sliderPosProportional = 1.0; - } - else - { - sliderPosProportional = valueToProportionOfLength (value); - jassert (sliderPosProportional >= 0 && sliderPosProportional <= 1.0); - } - } - else - { - sliderPosProportional = 0.5; - } - - if (style == LinearVertical || style == IncDecButtons) - sliderPosProportional = 1.0 - sliderPosProportional; - - return (float) (sliderRegionStart + sliderPosProportional * sliderRegionSize); -} - -bool Slider::isHorizontal() const throw() -{ - return style == LinearHorizontal - || style == LinearBar - || style == TwoValueHorizontal - || style == ThreeValueHorizontal; -} - -bool Slider::isVertical() const throw() -{ - return style == LinearVertical - || style == TwoValueVertical - || style == ThreeValueVertical; -} - -bool Slider::incDecDragDirectionIsHorizontal() const throw() -{ - return incDecButtonMode == incDecButtonsDraggable_Horizontal - || (incDecButtonMode == incDecButtonsDraggable_AutoDirection && incDecButtonsSideBySide); -} - -float Slider::getPositionOfValue (const double value) -{ - if (isHorizontal() || isVertical()) - { - return getLinearSliderPos (value); - } - else - { - jassertfalse // not a valid call on a slider that doesn't work linearly! - return 0.0f; - } -} - -//============================================================================== -void Slider::paint (Graphics& g) -{ - if (style != IncDecButtons) - { - if (style == Rotary || style == RotaryHorizontalDrag || style == RotaryVerticalDrag) - { - const float sliderPos = (float) valueToProportionOfLength (currentValue); - jassert (sliderPos >= 0 && sliderPos <= 1.0f); - - getLookAndFeel().drawRotarySlider (g, - sliderRect.getX(), - sliderRect.getY(), - sliderRect.getWidth(), - sliderRect.getHeight(), - sliderPos, - rotaryStart, rotaryEnd, - *this); - } - else - { - getLookAndFeel().drawLinearSlider (g, - sliderRect.getX(), - sliderRect.getY(), - sliderRect.getWidth(), - sliderRect.getHeight(), - getLinearSliderPos (currentValue), - getLinearSliderPos (valueMin), - getLinearSliderPos (valueMax), - style, - *this); - } - - if (style == LinearBar && valueBox == 0) - { - g.setColour (findColour (Slider::textBoxOutlineColourId)); - g.drawRect (0, 0, getWidth(), getHeight(), 1); - } - } -} - -void Slider::resized() -{ - int minXSpace = 0; - int minYSpace = 0; - - if (textBoxPos == TextBoxLeft || textBoxPos == TextBoxRight) - minXSpace = 30; - else - minYSpace = 15; - - const int tbw = jmax (0, jmin (textBoxWidth, getWidth() - minXSpace)); - const int tbh = jmax (0, jmin (textBoxHeight, getHeight() - minYSpace)); - - if (style == LinearBar) - { - if (valueBox != 0) - valueBox->setBounds (0, 0, getWidth(), getHeight()); - } - else - { - if (textBoxPos == NoTextBox) - { - sliderRect.setBounds (0, 0, getWidth(), getHeight()); - } - else if (textBoxPos == TextBoxLeft) - { - valueBox->setBounds (0, (getHeight() - tbh) / 2, tbw, tbh); - sliderRect.setBounds (tbw, 0, getWidth() - tbw, getHeight()); - } - else if (textBoxPos == TextBoxRight) - { - valueBox->setBounds (getWidth() - tbw, (getHeight() - tbh) / 2, tbw, tbh); - sliderRect.setBounds (0, 0, getWidth() - tbw, getHeight()); - } - else if (textBoxPos == TextBoxAbove) - { - valueBox->setBounds ((getWidth() - tbw) / 2, 0, tbw, tbh); - sliderRect.setBounds (0, tbh, getWidth(), getHeight() - tbh); - } - else if (textBoxPos == TextBoxBelow) - { - valueBox->setBounds ((getWidth() - tbw) / 2, getHeight() - tbh, tbw, tbh); - sliderRect.setBounds (0, 0, getWidth(), getHeight() - tbh); - } - } - - const int indent = getLookAndFeel().getSliderThumbRadius (*this); - - if (style == LinearBar) - { - const int barIndent = 1; - sliderRegionStart = barIndent; - sliderRegionSize = getWidth() - barIndent * 2; - - sliderRect.setBounds (sliderRegionStart, barIndent, - sliderRegionSize, getHeight() - barIndent * 2); - } - else if (isHorizontal()) - { - sliderRegionStart = sliderRect.getX() + indent; - sliderRegionSize = jmax (1, sliderRect.getWidth() - indent * 2); - - sliderRect.setBounds (sliderRegionStart, sliderRect.getY(), - sliderRegionSize, sliderRect.getHeight()); - } - else if (isVertical()) - { - sliderRegionStart = sliderRect.getY() + indent; - sliderRegionSize = jmax (1, sliderRect.getHeight() - indent * 2); - - sliderRect.setBounds (sliderRect.getX(), sliderRegionStart, - sliderRect.getWidth(), sliderRegionSize); - } - else - { - sliderRegionStart = 0; - sliderRegionSize = 100; - } - - if (style == IncDecButtons) - { - Rectangle buttonRect (sliderRect); - - if (textBoxPos == TextBoxLeft || textBoxPos == TextBoxRight) - buttonRect.expand (-2, 0); - else - buttonRect.expand (0, -2); - - incDecButtonsSideBySide = buttonRect.getWidth() > buttonRect.getHeight(); - - if (incDecButtonsSideBySide) - { - decButton->setBounds (buttonRect.getX(), - buttonRect.getY(), - buttonRect.getWidth() / 2, - buttonRect.getHeight()); - - decButton->setConnectedEdges (Button::ConnectedOnRight); - - incButton->setBounds (buttonRect.getCentreX(), - buttonRect.getY(), - buttonRect.getWidth() / 2, - buttonRect.getHeight()); - - incButton->setConnectedEdges (Button::ConnectedOnLeft); - } - else - { - incButton->setBounds (buttonRect.getX(), - buttonRect.getY(), - buttonRect.getWidth(), - buttonRect.getHeight() / 2); - - incButton->setConnectedEdges (Button::ConnectedOnBottom); - - decButton->setBounds (buttonRect.getX(), - buttonRect.getCentreY(), - buttonRect.getWidth(), - buttonRect.getHeight() / 2); - - decButton->setConnectedEdges (Button::ConnectedOnTop); - } - } -} - -void Slider::focusOfChildComponentChanged (FocusChangeType) -{ - repaint(); -} - -void Slider::mouseDown (const MouseEvent& e) -{ - mouseWasHidden = false; - incDecDragged = false; - - if (isEnabled()) - { - if (e.mods.isPopupMenu() && menuEnabled) - { - menuShown = true; - - PopupMenu m; - m.addItem (1, TRANS ("velocity-sensitive mode"), true, isVelocityBased); - m.addSeparator(); - - if (style == Rotary || style == RotaryHorizontalDrag || style == RotaryVerticalDrag) - { - PopupMenu rotaryMenu; - rotaryMenu.addItem (2, TRANS ("use circular dragging"), true, style == Rotary); - rotaryMenu.addItem (3, TRANS ("use left-right dragging"), true, style == RotaryHorizontalDrag); - rotaryMenu.addItem (4, TRANS ("use up-down dragging"), true, style == RotaryVerticalDrag); - - m.addSubMenu (TRANS ("rotary mode"), rotaryMenu); - } - - const int r = m.show(); - - if (r == 1) - { - setVelocityBasedMode (! isVelocityBased); - } - else if (r == 2) - { - setSliderStyle (Rotary); - } - else if (r == 3) - { - setSliderStyle (RotaryHorizontalDrag); - } - else if (r == 4) - { - setSliderStyle (RotaryVerticalDrag); - } - } - else if (maximum > minimum) - { - menuShown = false; - - if (valueBox != 0) - valueBox->hideEditor (true); - - sliderBeingDragged = 0; - - if (style == TwoValueHorizontal - || style == TwoValueVertical - || style == ThreeValueHorizontal - || style == ThreeValueVertical) - { - const float mousePos = (float) (isVertical() ? e.y : e.x); - - const float normalPosDistance = fabsf (getLinearSliderPos (currentValue) - mousePos); - const float minPosDistance = fabsf (getLinearSliderPos (valueMin) - 0.1f - mousePos); - const float maxPosDistance = fabsf (getLinearSliderPos (valueMax) + 0.1f - mousePos); - - if (style == TwoValueHorizontal || style == TwoValueVertical) - { - if (maxPosDistance <= minPosDistance) - sliderBeingDragged = 2; - else - sliderBeingDragged = 1; - } - else if (style == ThreeValueHorizontal || style == ThreeValueVertical) - { - if (normalPosDistance >= minPosDistance && maxPosDistance >= minPosDistance) - sliderBeingDragged = 1; - else if (normalPosDistance >= maxPosDistance) - sliderBeingDragged = 2; - } - } - - minMaxDiff = valueMax - valueMin; - - mouseXWhenLastDragged = e.x; - mouseYWhenLastDragged = e.y; - lastAngle = rotaryStart + (rotaryEnd - rotaryStart) - * valueToProportionOfLength (currentValue); - - if (sliderBeingDragged == 2) - valueWhenLastDragged = valueMax; - else if (sliderBeingDragged == 1) - valueWhenLastDragged = valueMin; - else - valueWhenLastDragged = currentValue; - - valueOnMouseDown = valueWhenLastDragged; - - if (popupDisplayEnabled) - { - SliderPopupDisplayComponent* const popup = new SliderPopupDisplayComponent (this); - popupDisplay = popup; - - if (parentForPopupDisplay != 0) - { - parentForPopupDisplay->addChildComponent (popup); - } - else - { - popup->addToDesktop (0); - } - - popup->setVisible (true); - } - - sendDragStart(); - - mouseDrag (e); - } - } -} - -void Slider::mouseUp (const MouseEvent&) -{ - if (isEnabled() - && (! menuShown) - && (maximum > minimum) - && (style != IncDecButtons || incDecDragged)) - { - restoreMouseIfHidden(); - - if (sendChangeOnlyOnRelease && valueOnMouseDown != currentValue) - triggerChangeMessage (false); - - sendDragEnd(); - - deleteAndZero (popupDisplay); - - if (style == IncDecButtons) - { - incButton->setState (Button::buttonNormal); - decButton->setState (Button::buttonNormal); - } - } -} - -void Slider::restoreMouseIfHidden() -{ - if (mouseWasHidden) - { - mouseWasHidden = false; - - Component* c = Component::getComponentUnderMouse(); - - if (c == 0) - c = this; - - c->enableUnboundedMouseMovement (false); - - const double pos = (sliderBeingDragged == 2) ? getMaxValue() - : ((sliderBeingDragged == 1) ? getMinValue() - : currentValue); - - const int pixelPos = (int) getLinearSliderPos (pos); - - int x = isHorizontal() ? pixelPos : (getWidth() / 2); - int y = isVertical() ? pixelPos : (getHeight() / 2); - - relativePositionToGlobal (x, y); - Desktop::setMousePosition (x, y); - } -} - -void Slider::modifierKeysChanged (const ModifierKeys& modifiers) -{ - if (isEnabled() - && style != IncDecButtons - && style != Rotary - && isVelocityBased == modifiers.isAnyModifierKeyDown()) - { - restoreMouseIfHidden(); - } -} - -static double smallestAngleBetween (double a1, double a2) -{ - return jmin (fabs (a1 - a2), - fabs (a1 + double_Pi * 2.0 - a2), - fabs (a2 + double_Pi * 2.0 - a1)); -} - -void Slider::mouseDrag (const MouseEvent& e) -{ - if (isEnabled() - && (! menuShown) - && (maximum > minimum)) - { - if (style == Rotary) - { - int dx = e.x - sliderRect.getCentreX(); - int dy = e.y - sliderRect.getCentreY(); - - if (dx * dx + dy * dy > 25) - { - double angle = atan2 ((double) dx, (double) -dy); - while (angle < 0.0) - angle += double_Pi * 2.0; - - if (rotaryStop && ! e.mouseWasClicked()) - { - if (fabs (angle - lastAngle) > double_Pi) - { - if (angle >= lastAngle) - angle -= double_Pi * 2.0; - else - angle += double_Pi * 2.0; - } - - if (angle >= lastAngle) - angle = jmin (angle, (double) jmax (rotaryStart, rotaryEnd)); - else - angle = jmax (angle, (double) jmin (rotaryStart, rotaryEnd)); - } - else - { - while (angle < rotaryStart) - angle += double_Pi * 2.0; - - if (angle > rotaryEnd) - { - if (smallestAngleBetween (angle, rotaryStart) <= smallestAngleBetween (angle, rotaryEnd)) - angle = rotaryStart; - else - angle = rotaryEnd; - } - } - - const double proportion = (angle - rotaryStart) / (rotaryEnd - rotaryStart); - - valueWhenLastDragged = proportionOfLengthToValue (jlimit (0.0, 1.0, proportion)); - - lastAngle = angle; - } - } - else - { - if (style == LinearBar && e.mouseWasClicked() - && valueBox != 0 && valueBox->isEditable()) - return; - - if (style == IncDecButtons) - { - if (! incDecDragged) - incDecDragged = e.getDistanceFromDragStart() > 10 && ! e.mouseWasClicked(); - - if (! incDecDragged) - return; - } - - - if ((isVelocityBased == (userKeyOverridesVelocity ? e.mods.testFlags (ModifierKeys::ctrlModifier | ModifierKeys::commandModifier | ModifierKeys::altModifier) - : false)) - || ((maximum - minimum) / sliderRegionSize < interval)) - { - const int mousePos = (isHorizontal() || style == RotaryHorizontalDrag) ? e.x : e.y; - - double scaledMousePos = (mousePos - sliderRegionStart) / (double) sliderRegionSize; - - if (style == RotaryHorizontalDrag - || style == RotaryVerticalDrag - || style == IncDecButtons - || ((style == LinearHorizontal || style == LinearVertical || style == LinearBar) - && ! snapsToMousePos)) - { - const int mouseDiff = (style == RotaryHorizontalDrag - || style == LinearHorizontal - || style == LinearBar - || (style == IncDecButtons && incDecDragDirectionIsHorizontal())) - ? e.getDistanceFromDragStartX() - : -e.getDistanceFromDragStartY(); - - double newPos = valueToProportionOfLength (valueOnMouseDown) - + mouseDiff * (1.0 / pixelsForFullDragExtent); - - valueWhenLastDragged = proportionOfLengthToValue (jlimit (0.0, 1.0, newPos)); - - if (style == IncDecButtons) - { - incButton->setState (mouseDiff < 0 ? Button::buttonNormal : Button::buttonDown); - decButton->setState (mouseDiff > 0 ? Button::buttonNormal : Button::buttonDown); - } - } - else - { - if (style == LinearVertical) - scaledMousePos = 1.0 - scaledMousePos; - - valueWhenLastDragged = proportionOfLengthToValue (jlimit (0.0, 1.0, scaledMousePos)); - } - } - else - { - const int mouseDiff = (isHorizontal() || style == RotaryHorizontalDrag - || (style == IncDecButtons && incDecDragDirectionIsHorizontal())) - ? e.x - mouseXWhenLastDragged - : e.y - mouseYWhenLastDragged; - - const double maxSpeed = jmax (200, sliderRegionSize); - double speed = jlimit (0.0, maxSpeed, (double) abs (mouseDiff)); - - if (speed != 0) - { - speed = 0.2 * velocityModeSensitivity - * (1.0 + sin (double_Pi * (1.5 + jmin (0.5, velocityModeOffset - + jmax (0.0, (double) (speed - velocityModeThreshold)) - / maxSpeed)))); - - if (mouseDiff < 0) - speed = -speed; - - if (style == LinearVertical || style == RotaryVerticalDrag - || (style == IncDecButtons && ! incDecDragDirectionIsHorizontal())) - speed = -speed; - - const double currentPos = valueToProportionOfLength (valueWhenLastDragged); - - valueWhenLastDragged = proportionOfLengthToValue (jlimit (0.0, 1.0, currentPos + speed)); - - e.originalComponent->enableUnboundedMouseMovement (true, false); - mouseWasHidden = true; - } - } - } - - valueWhenLastDragged = jlimit (minimum, maximum, valueWhenLastDragged); - - if (sliderBeingDragged == 0) - { - setValue (snapValue (valueWhenLastDragged, true), - ! sendChangeOnlyOnRelease, true); - } - else if (sliderBeingDragged == 1) - { - setMinValue (snapValue (valueWhenLastDragged, true), - ! sendChangeOnlyOnRelease, false); - - if (e.mods.isShiftDown()) - setMaxValue (getMinValue() + minMaxDiff, false); - else - minMaxDiff = valueMax - valueMin; - } - else - { - jassert (sliderBeingDragged == 2); - - setMaxValue (snapValue (valueWhenLastDragged, true), - ! sendChangeOnlyOnRelease, false); - - if (e.mods.isShiftDown()) - setMinValue (getMaxValue() - minMaxDiff, false); - else - minMaxDiff = valueMax - valueMin; - } - - mouseXWhenLastDragged = e.x; - mouseYWhenLastDragged = e.y; - } -} - -void Slider::mouseDoubleClick (const MouseEvent&) -{ - if (doubleClickToValue - && isEnabled() - && style != IncDecButtons - && minimum <= doubleClickReturnValue - && maximum >= doubleClickReturnValue) - { - sendDragStart(); - setValue (doubleClickReturnValue, true, true); - sendDragEnd(); - } -} - -void Slider::mouseWheelMove (const MouseEvent& e, float wheelIncrementX, float wheelIncrementY) -{ - if (scrollWheelEnabled && isEnabled()) - { - if (maximum > minimum && ! isMouseButtonDownAnywhere()) - { - if (valueBox != 0) - valueBox->hideEditor (false); - - const double proportionDelta = (wheelIncrementX != 0 ? -wheelIncrementX : wheelIncrementY) * 0.15f; - const double currentPos = valueToProportionOfLength (currentValue); - const double newValue = proportionOfLengthToValue (jlimit (0.0, 1.0, currentPos + proportionDelta)); - - double delta = (newValue != currentValue) - ? jmax (fabs (newValue - currentValue), interval) : 0; - - if (currentValue > newValue) - delta = -delta; - - sendDragStart(); - setValue (snapValue (currentValue + delta, false), true, true); - sendDragEnd(); - } - } - else - { - Component::mouseWheelMove (e, wheelIncrementX, wheelIncrementY); - } -} - -void SliderListener::sliderDragStarted (Slider*) -{ -} - -void SliderListener::sliderDragEnded (Slider*) -{ -} - - -END_JUCE_NAMESPACE +/* + ============================================================================== + + This file is part of the JUCE library - "Jules' Utility Class Extensions" + Copyright 2004-7 by Raw Material Software ltd. + + ------------------------------------------------------------------------------ + + JUCE can be redistributed and/or modified 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. + + JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA + + ------------------------------------------------------------------------------ + + If you'd like to release a closed-source product which uses JUCE, commercial + licenses are also available: visit www.rawmaterialsoftware.com/juce for + more information. + + ============================================================================== +*/ + +#include "../../../../juce_core/basics/juce_StandardHeader.h" + +BEGIN_JUCE_NAMESPACE + +#include "juce_Slider.h" +#include "../lookandfeel/juce_LookAndFeel.h" +#include "../menus/juce_PopupMenu.h" +#include "../juce_Desktop.h" +#include "../special/juce_BubbleComponent.h" +#include "../../../../juce_core/text/juce_LocalisedStrings.h" + + +//============================================================================== +class SliderPopupDisplayComponent : public BubbleComponent +{ +public: + //============================================================================== + SliderPopupDisplayComponent (Slider* const owner_) + : owner (owner_), + font (15.0f, Font::bold) + { + setAlwaysOnTop (true); + } + + ~SliderPopupDisplayComponent() + { + } + + void paintContent (Graphics& g, int w, int h) + { + g.setFont (font); + g.setColour (Colours::black); + + g.drawFittedText (text, 0, 0, w, h, Justification::centred, 1); + } + + void getContentSize (int& w, int& h) + { + w = font.getStringWidth (text) + 18; + h = (int) (font.getHeight() * 1.6f); + } + + void updatePosition (const String& newText) + { + if (text != newText) + { + text = newText; + repaint(); + } + + BubbleComponent::setPosition (owner); + } + + //============================================================================== + juce_UseDebuggingNewOperator + +private: + Slider* owner; + Font font; + String text; + + SliderPopupDisplayComponent (const SliderPopupDisplayComponent&); + const SliderPopupDisplayComponent& operator= (const SliderPopupDisplayComponent&); +}; + +//============================================================================== +Slider::Slider (const String& name) + : Component (name), + listeners (2), + currentValue (0.0), + valueMin (0.0), + valueMax (0.0), + minimum (0), + maximum (10), + interval (0), + skewFactor (1.0), + velocityModeSensitivity (1.0), + velocityModeOffset (0.0), + velocityModeThreshold (1), + rotaryStart (float_Pi * 1.2f), + rotaryEnd (float_Pi * 2.8f), + numDecimalPlaces (7), + sliderRegionStart (0), + sliderRegionSize (1), + sliderBeingDragged (-1), + pixelsForFullDragExtent (250), + style (LinearHorizontal), + textBoxPos (TextBoxLeft), + textBoxWidth (80), + textBoxHeight (20), + incDecButtonMode (incDecButtonsNotDraggable), + editableText (true), + doubleClickToValue (false), + isVelocityBased (false), + userKeyOverridesVelocity (true), + rotaryStop (true), + incDecButtonsSideBySide (false), + sendChangeOnlyOnRelease (false), + popupDisplayEnabled (false), + menuEnabled (false), + menuShown (false), + scrollWheelEnabled (true), + snapsToMousePos (true), + valueBox (0), + incButton (0), + decButton (0), + popupDisplay (0), + parentForPopupDisplay (0) +{ + setWantsKeyboardFocus (false); + setRepaintsOnMouseActivity (true); + + lookAndFeelChanged(); + updateText(); +} + +Slider::~Slider() +{ + deleteAndZero (popupDisplay); + deleteAllChildren(); +} + + +//============================================================================== +void Slider::handleAsyncUpdate() +{ + cancelPendingUpdate(); + + for (int i = listeners.size(); --i >= 0;) + { + ((SliderListener*) listeners.getUnchecked (i))->sliderValueChanged (this); + i = jmin (i, listeners.size()); + } +} + +void Slider::sendDragStart() +{ + startedDragging(); + + for (int i = listeners.size(); --i >= 0;) + { + ((SliderListener*) listeners.getUnchecked (i))->sliderDragStarted (this); + i = jmin (i, listeners.size()); + } +} + +void Slider::sendDragEnd() +{ + stoppedDragging(); + + sliderBeingDragged = -1; + + for (int i = listeners.size(); --i >= 0;) + { + ((SliderListener*) listeners.getUnchecked (i))->sliderDragEnded (this); + i = jmin (i, listeners.size()); + } +} + +void Slider::addListener (SliderListener* const listener) throw() +{ + jassert (listener != 0); + if (listener != 0) + listeners.add (listener); +} + +void Slider::removeListener (SliderListener* const listener) throw() +{ + listeners.removeValue (listener); +} + +//============================================================================== +void Slider::setSliderStyle (const SliderStyle newStyle) +{ + if (style != newStyle) + { + style = newStyle; + repaint(); + lookAndFeelChanged(); + } +} + +void Slider::setRotaryParameters (const float startAngleRadians, + const float endAngleRadians, + const bool stopAtEnd) +{ + // make sure the values are sensible.. + jassert (rotaryStart >= 0 && rotaryEnd >= 0); + jassert (rotaryStart < float_Pi * 4.0f && rotaryEnd < float_Pi * 4.0f); + jassert (rotaryStart < rotaryEnd); + + rotaryStart = startAngleRadians; + rotaryEnd = endAngleRadians; + rotaryStop = stopAtEnd; +} + +void Slider::setVelocityBasedMode (const bool velBased) throw() +{ + isVelocityBased = velBased; +} + +void Slider::setVelocityModeParameters (const double sensitivity, + const int threshold, + const double offset, + const bool userCanPressKeyToSwapMode) throw() +{ + jassert (threshold >= 0); + jassert (sensitivity > 0); + jassert (offset >= 0); + + velocityModeSensitivity = sensitivity; + velocityModeOffset = offset; + velocityModeThreshold = threshold; + userKeyOverridesVelocity = userCanPressKeyToSwapMode; +} + +void Slider::setSkewFactor (const double factor) throw() +{ + skewFactor = factor; +} + +void Slider::setSkewFactorFromMidPoint (const double sliderValueToShowAtMidPoint) throw() +{ + if (maximum > minimum) + skewFactor = log (0.5) / log ((sliderValueToShowAtMidPoint - minimum) + / (maximum - minimum)); +} + +void Slider::setMouseDragSensitivity (const int distanceForFullScaleDrag) +{ + jassert (distanceForFullScaleDrag > 0); + + pixelsForFullDragExtent = distanceForFullScaleDrag; +} + +void Slider::setIncDecButtonsMode (const IncDecButtonMode mode) +{ + if (incDecButtonMode != mode) + { + incDecButtonMode = mode; + lookAndFeelChanged(); + } +} + +void Slider::setTextBoxStyle (const TextEntryBoxPosition newPosition, + const bool isReadOnly, + const int textEntryBoxWidth, + const int textEntryBoxHeight) +{ + textBoxPos = newPosition; + editableText = ! isReadOnly; + textBoxWidth = textEntryBoxWidth; + textBoxHeight = textEntryBoxHeight; + + repaint(); + lookAndFeelChanged(); +} + +void Slider::setTextBoxIsEditable (const bool shouldBeEditable) throw() +{ + editableText = shouldBeEditable; + + if (valueBox != 0) + valueBox->setEditable (shouldBeEditable && isEnabled()); +} + +void Slider::showTextBox() +{ + jassert (editableText); // this should probably be avoided in read-only sliders. + + if (valueBox != 0) + valueBox->showEditor(); +} + +void Slider::hideTextBox (const bool discardCurrentEditorContents) +{ + if (valueBox != 0) + { + valueBox->hideEditor (discardCurrentEditorContents); + + if (discardCurrentEditorContents) + updateText(); + } +} + +void Slider::setChangeNotificationOnlyOnRelease (const bool onlyNotifyOnRelease) throw() +{ + sendChangeOnlyOnRelease = onlyNotifyOnRelease; +} + +void Slider::setSliderSnapsToMousePosition (const bool shouldSnapToMouse) throw() +{ + snapsToMousePos = shouldSnapToMouse; +} + +void Slider::setPopupDisplayEnabled (const bool enabled, + Component* const parentComponentToUse) throw() +{ + popupDisplayEnabled = enabled; + parentForPopupDisplay = parentComponentToUse; +} + +//============================================================================== +void Slider::colourChanged() +{ + lookAndFeelChanged(); +} + +void Slider::lookAndFeelChanged() +{ + const String previousTextBoxContent (valueBox != 0 ? valueBox->getText() + : getTextFromValue (currentValue)); + + deleteAllChildren(); + valueBox = 0; + + LookAndFeel& lf = getLookAndFeel(); + + if (textBoxPos != NoTextBox) + { + addAndMakeVisible (valueBox = getLookAndFeel().createSliderTextBox (*this)); + + valueBox->setWantsKeyboardFocus (false); + valueBox->setText (previousTextBoxContent, false); + + valueBox->setEditable (editableText && isEnabled()); + valueBox->addListener (this); + + if (style == LinearBar) + valueBox->addMouseListener (this, false); + } + + if (style == IncDecButtons) + { + addAndMakeVisible (incButton = lf.createSliderButton (true)); + incButton->addButtonListener (this); + + addAndMakeVisible (decButton = lf.createSliderButton (false)); + decButton->addButtonListener (this); + + if (incDecButtonMode != incDecButtonsNotDraggable) + { + incButton->addMouseListener (this, false); + decButton->addMouseListener (this, false); + } + else + { + incButton->setRepeatSpeed (300, 100, 20); + incButton->addMouseListener (decButton, false); + + decButton->setRepeatSpeed (300, 100, 20); + decButton->addMouseListener (incButton, false); + } + } + + setComponentEffect (lf.getSliderEffect()); + + resized(); + repaint(); +} + +//============================================================================== +void Slider::setRange (const double newMin, + const double newMax, + const double newInt) +{ + if (minimum != newMin + || maximum != newMax + || interval != newInt) + { + minimum = newMin; + maximum = newMax; + interval = newInt; + + // figure out the number of DPs needed to display all values at this + // interval setting. + numDecimalPlaces = 7; + + if (newInt != 0) + { + int v = abs ((int) (newInt * 10000000)); + + while ((v % 10) == 0) + { + --numDecimalPlaces; + v /= 10; + } + } + + // keep the current values inside the new range.. + if (style != TwoValueHorizontal && style != TwoValueVertical) + { + setValue (currentValue, false, false); + } + else + { + setMinValue (getMinValue(), false, false); + setMaxValue (getMaxValue(), false, false); + } + + updateText(); + } +} + +void Slider::triggerChangeMessage (const bool synchronous) +{ + if (synchronous) + handleAsyncUpdate(); + else + triggerAsyncUpdate(); + + valueChanged(); +} + +double Slider::getValue() const throw() +{ + // for a two-value style slider, you should use the getMinValue() and getMaxValue() + // methods to get the two values. + jassert (style != TwoValueHorizontal && style != TwoValueVertical); + + return currentValue; +} + +void Slider::setValue (double newValue, + const bool sendUpdateMessage, + const bool sendMessageSynchronously) +{ + // for a two-value style slider, you should use the setMinValue() and setMaxValue() + // methods to set the two values. + jassert (style != TwoValueHorizontal && style != TwoValueVertical); + + newValue = constrainedValue (newValue); + + if (style == ThreeValueHorizontal || style == ThreeValueVertical) + { + jassert (valueMin <= valueMax); + newValue = jlimit (valueMin, valueMax, newValue); + } + + if (currentValue != newValue) + { + if (valueBox != 0) + valueBox->hideEditor (true); + + currentValue = newValue; + updateText(); + repaint(); + + if (popupDisplay != 0) + { + ((SliderPopupDisplayComponent*) popupDisplay)->updatePosition (getTextFromValue (currentValue)); + popupDisplay->repaint(); + } + + if (sendUpdateMessage) + triggerChangeMessage (sendMessageSynchronously); + } +} + +double Slider::getMinValue() const throw() +{ + // The minimum value only applies to sliders that are in two- or three-value mode. + jassert (style == TwoValueHorizontal || style == TwoValueVertical + || style == ThreeValueHorizontal || style == ThreeValueVertical); + + return valueMin; +} + +double Slider::getMaxValue() const throw() +{ + // The maximum value only applies to sliders that are in two- or three-value mode. + jassert (style == TwoValueHorizontal || style == TwoValueVertical + || style == ThreeValueHorizontal || style == ThreeValueVertical); + + return valueMax; +} + +void Slider::setMinValue (double newValue, const bool sendUpdateMessage, const bool sendMessageSynchronously) +{ + // The minimum value only applies to sliders that are in two- or three-value mode. + jassert (style == TwoValueHorizontal || style == TwoValueVertical + || style == ThreeValueHorizontal || style == ThreeValueVertical); + + newValue = constrainedValue (newValue); + + if (style == TwoValueHorizontal || style == TwoValueVertical) + newValue = jmin (valueMax, newValue); + else + newValue = jmin (currentValue, newValue); + + if (valueMin != newValue) + { + valueMin = newValue; + repaint(); + + if (popupDisplay != 0) + { + ((SliderPopupDisplayComponent*) popupDisplay)->updatePosition (getTextFromValue (valueMin)); + popupDisplay->repaint(); + } + + if (sendUpdateMessage) + triggerChangeMessage (sendMessageSynchronously); + } +} + +void Slider::setMaxValue (double newValue, const bool sendUpdateMessage, const bool sendMessageSynchronously) +{ + // The maximum value only applies to sliders that are in two- or three-value mode. + jassert (style == TwoValueHorizontal || style == TwoValueVertical + || style == ThreeValueHorizontal || style == ThreeValueVertical); + + newValue = constrainedValue (newValue); + + if (style == TwoValueHorizontal || style == TwoValueVertical) + newValue = jmax (valueMin, newValue); + else + newValue = jmax (currentValue, newValue); + + if (valueMax != newValue) + { + valueMax = newValue; + repaint(); + + if (popupDisplay != 0) + { + ((SliderPopupDisplayComponent*) popupDisplay)->updatePosition (getTextFromValue (valueMax)); + popupDisplay->repaint(); + } + + if (sendUpdateMessage) + triggerChangeMessage (sendMessageSynchronously); + } +} + +void Slider::setDoubleClickReturnValue (const bool isDoubleClickEnabled, + const double valueToSetOnDoubleClick) throw() +{ + doubleClickToValue = isDoubleClickEnabled; + doubleClickReturnValue = valueToSetOnDoubleClick; +} + +double Slider::getDoubleClickReturnValue (bool& isEnabled_) const throw() +{ + isEnabled_ = doubleClickToValue; + return doubleClickReturnValue; +} + +void Slider::updateText() +{ + if (valueBox != 0) + valueBox->setText (getTextFromValue (currentValue), false); +} + +void Slider::setTextValueSuffix (const String& suffix) +{ + if (textSuffix != suffix) + { + textSuffix = suffix; + updateText(); + } +} + +const String Slider::getTextFromValue (double v) +{ + if (numDecimalPlaces > 0) + return String (v, numDecimalPlaces) + textSuffix; + else + return String (roundDoubleToInt (v)) + textSuffix; +} + +double Slider::getValueFromText (const String& text) +{ + String t (text.trimStart()); + + if (t.endsWith (textSuffix)) + t = t.substring (0, t.length() - textSuffix.length()); + + while (t.startsWithChar (T('+'))) + t = t.substring (1).trimStart(); + + return t.initialSectionContainingOnly (T("0123456789.,-")) + .getDoubleValue(); +} + +double Slider::proportionOfLengthToValue (double proportion) +{ + if (skewFactor != 1.0 && proportion > 0.0) + proportion = exp (log (proportion) / skewFactor); + + return minimum + (maximum - minimum) * proportion; +} + +double Slider::valueToProportionOfLength (double value) +{ + const double n = (value - minimum) / (maximum - minimum); + + return skewFactor == 1.0 ? n : pow (n, skewFactor); +} + +double Slider::snapValue (double attemptedValue, const bool) +{ + return attemptedValue; +} + +//============================================================================== +void Slider::startedDragging() +{ +} + +void Slider::stoppedDragging() +{ +} + +void Slider::valueChanged() +{ +} + +//============================================================================== +void Slider::enablementChanged() +{ + repaint(); +} + +void Slider::setPopupMenuEnabled (const bool menuEnabled_) throw() +{ + menuEnabled = menuEnabled_; +} + +void Slider::setScrollWheelEnabled (const bool enabled) throw() +{ + scrollWheelEnabled = enabled; +} + +//============================================================================== +void Slider::labelTextChanged (Label* label) +{ + const double newValue = snapValue (getValueFromText (label->getText()), false); + + if (getValue() != newValue) + { + sendDragStart(); + setValue (newValue, true, true); + sendDragEnd(); + } + + updateText(); // force a clean-up of the text, needed in case setValue() hasn't done this. +} + +void Slider::buttonClicked (Button* button) +{ + if (style == IncDecButtons) + { + sendDragStart(); + + if (button == incButton) + setValue (snapValue (getValue() + interval, false), true, true); + else if (button == decButton) + setValue (snapValue (getValue() - interval, false), true, true); + + sendDragEnd(); + } +} + +//============================================================================== +double Slider::constrainedValue (double value) const throw() +{ + if (interval > 0) + value = minimum + interval * floor ((value - minimum) / interval + 0.5); + + if (value <= minimum || maximum <= minimum) + value = minimum; + else if (value >= maximum) + value = maximum; + + return value; +} + +float Slider::getLinearSliderPos (const double value) +{ + double sliderPosProportional; + + if (maximum > minimum) + { + if (value < minimum) + { + sliderPosProportional = 0.0; + } + else if (value > maximum) + { + sliderPosProportional = 1.0; + } + else + { + sliderPosProportional = valueToProportionOfLength (value); + jassert (sliderPosProportional >= 0 && sliderPosProportional <= 1.0); + } + } + else + { + sliderPosProportional = 0.5; + } + + if (style == LinearVertical || style == IncDecButtons) + sliderPosProportional = 1.0 - sliderPosProportional; + + return (float) (sliderRegionStart + sliderPosProportional * sliderRegionSize); +} + +bool Slider::isHorizontal() const throw() +{ + return style == LinearHorizontal + || style == LinearBar + || style == TwoValueHorizontal + || style == ThreeValueHorizontal; +} + +bool Slider::isVertical() const throw() +{ + return style == LinearVertical + || style == TwoValueVertical + || style == ThreeValueVertical; +} + +bool Slider::incDecDragDirectionIsHorizontal() const throw() +{ + return incDecButtonMode == incDecButtonsDraggable_Horizontal + || (incDecButtonMode == incDecButtonsDraggable_AutoDirection && incDecButtonsSideBySide); +} + +float Slider::getPositionOfValue (const double value) +{ + if (isHorizontal() || isVertical()) + { + return getLinearSliderPos (value); + } + else + { + jassertfalse // not a valid call on a slider that doesn't work linearly! + return 0.0f; + } +} + +//============================================================================== +void Slider::paint (Graphics& g) +{ + if (style != IncDecButtons) + { + if (style == Rotary || style == RotaryHorizontalDrag || style == RotaryVerticalDrag) + { + const float sliderPos = (float) valueToProportionOfLength (currentValue); + jassert (sliderPos >= 0 && sliderPos <= 1.0f); + + getLookAndFeel().drawRotarySlider (g, + sliderRect.getX(), + sliderRect.getY(), + sliderRect.getWidth(), + sliderRect.getHeight(), + sliderPos, + rotaryStart, rotaryEnd, + *this); + } + else + { + getLookAndFeel().drawLinearSlider (g, + sliderRect.getX(), + sliderRect.getY(), + sliderRect.getWidth(), + sliderRect.getHeight(), + getLinearSliderPos (currentValue), + getLinearSliderPos (valueMin), + getLinearSliderPos (valueMax), + style, + *this); + } + + if (style == LinearBar && valueBox == 0) + { + g.setColour (findColour (Slider::textBoxOutlineColourId)); + g.drawRect (0, 0, getWidth(), getHeight(), 1); + } + } +} + +void Slider::resized() +{ + int minXSpace = 0; + int minYSpace = 0; + + if (textBoxPos == TextBoxLeft || textBoxPos == TextBoxRight) + minXSpace = 30; + else + minYSpace = 15; + + const int tbw = jmax (0, jmin (textBoxWidth, getWidth() - minXSpace)); + const int tbh = jmax (0, jmin (textBoxHeight, getHeight() - minYSpace)); + + if (style == LinearBar) + { + if (valueBox != 0) + valueBox->setBounds (0, 0, getWidth(), getHeight()); + } + else + { + if (textBoxPos == NoTextBox) + { + sliderRect.setBounds (0, 0, getWidth(), getHeight()); + } + else if (textBoxPos == TextBoxLeft) + { + valueBox->setBounds (0, (getHeight() - tbh) / 2, tbw, tbh); + sliderRect.setBounds (tbw, 0, getWidth() - tbw, getHeight()); + } + else if (textBoxPos == TextBoxRight) + { + valueBox->setBounds (getWidth() - tbw, (getHeight() - tbh) / 2, tbw, tbh); + sliderRect.setBounds (0, 0, getWidth() - tbw, getHeight()); + } + else if (textBoxPos == TextBoxAbove) + { + valueBox->setBounds ((getWidth() - tbw) / 2, 0, tbw, tbh); + sliderRect.setBounds (0, tbh, getWidth(), getHeight() - tbh); + } + else if (textBoxPos == TextBoxBelow) + { + valueBox->setBounds ((getWidth() - tbw) / 2, getHeight() - tbh, tbw, tbh); + sliderRect.setBounds (0, 0, getWidth(), getHeight() - tbh); + } + } + + const int indent = getLookAndFeel().getSliderThumbRadius (*this); + + if (style == LinearBar) + { + const int barIndent = 1; + sliderRegionStart = barIndent; + sliderRegionSize = getWidth() - barIndent * 2; + + sliderRect.setBounds (sliderRegionStart, barIndent, + sliderRegionSize, getHeight() - barIndent * 2); + } + else if (isHorizontal()) + { + sliderRegionStart = sliderRect.getX() + indent; + sliderRegionSize = jmax (1, sliderRect.getWidth() - indent * 2); + + sliderRect.setBounds (sliderRegionStart, sliderRect.getY(), + sliderRegionSize, sliderRect.getHeight()); + } + else if (isVertical()) + { + sliderRegionStart = sliderRect.getY() + indent; + sliderRegionSize = jmax (1, sliderRect.getHeight() - indent * 2); + + sliderRect.setBounds (sliderRect.getX(), sliderRegionStart, + sliderRect.getWidth(), sliderRegionSize); + } + else + { + sliderRegionStart = 0; + sliderRegionSize = 100; + } + + if (style == IncDecButtons) + { + Rectangle buttonRect (sliderRect); + + if (textBoxPos == TextBoxLeft || textBoxPos == TextBoxRight) + buttonRect.expand (-2, 0); + else + buttonRect.expand (0, -2); + + incDecButtonsSideBySide = buttonRect.getWidth() > buttonRect.getHeight(); + + if (incDecButtonsSideBySide) + { + decButton->setBounds (buttonRect.getX(), + buttonRect.getY(), + buttonRect.getWidth() / 2, + buttonRect.getHeight()); + + decButton->setConnectedEdges (Button::ConnectedOnRight); + + incButton->setBounds (buttonRect.getCentreX(), + buttonRect.getY(), + buttonRect.getWidth() / 2, + buttonRect.getHeight()); + + incButton->setConnectedEdges (Button::ConnectedOnLeft); + } + else + { + incButton->setBounds (buttonRect.getX(), + buttonRect.getY(), + buttonRect.getWidth(), + buttonRect.getHeight() / 2); + + incButton->setConnectedEdges (Button::ConnectedOnBottom); + + decButton->setBounds (buttonRect.getX(), + buttonRect.getCentreY(), + buttonRect.getWidth(), + buttonRect.getHeight() / 2); + + decButton->setConnectedEdges (Button::ConnectedOnTop); + } + } +} + +void Slider::focusOfChildComponentChanged (FocusChangeType) +{ + repaint(); +} + +void Slider::mouseDown (const MouseEvent& e) +{ + mouseWasHidden = false; + incDecDragged = false; + + if (isEnabled()) + { + if (e.mods.isPopupMenu() && menuEnabled) + { + menuShown = true; + + PopupMenu m; + m.addItem (1, TRANS ("velocity-sensitive mode"), true, isVelocityBased); + m.addSeparator(); + + if (style == Rotary || style == RotaryHorizontalDrag || style == RotaryVerticalDrag) + { + PopupMenu rotaryMenu; + rotaryMenu.addItem (2, TRANS ("use circular dragging"), true, style == Rotary); + rotaryMenu.addItem (3, TRANS ("use left-right dragging"), true, style == RotaryHorizontalDrag); + rotaryMenu.addItem (4, TRANS ("use up-down dragging"), true, style == RotaryVerticalDrag); + + m.addSubMenu (TRANS ("rotary mode"), rotaryMenu); + } + + const int r = m.show(); + + if (r == 1) + { + setVelocityBasedMode (! isVelocityBased); + } + else if (r == 2) + { + setSliderStyle (Rotary); + } + else if (r == 3) + { + setSliderStyle (RotaryHorizontalDrag); + } + else if (r == 4) + { + setSliderStyle (RotaryVerticalDrag); + } + } + else if (maximum > minimum) + { + menuShown = false; + + if (valueBox != 0) + valueBox->hideEditor (true); + + sliderBeingDragged = 0; + + if (style == TwoValueHorizontal + || style == TwoValueVertical + || style == ThreeValueHorizontal + || style == ThreeValueVertical) + { + const float mousePos = (float) (isVertical() ? e.y : e.x); + + const float normalPosDistance = fabsf (getLinearSliderPos (currentValue) - mousePos); + const float minPosDistance = fabsf (getLinearSliderPos (valueMin) - 0.1f - mousePos); + const float maxPosDistance = fabsf (getLinearSliderPos (valueMax) + 0.1f - mousePos); + + if (style == TwoValueHorizontal || style == TwoValueVertical) + { + if (maxPosDistance <= minPosDistance) + sliderBeingDragged = 2; + else + sliderBeingDragged = 1; + } + else if (style == ThreeValueHorizontal || style == ThreeValueVertical) + { + if (normalPosDistance >= minPosDistance && maxPosDistance >= minPosDistance) + sliderBeingDragged = 1; + else if (normalPosDistance >= maxPosDistance) + sliderBeingDragged = 2; + } + } + + minMaxDiff = valueMax - valueMin; + + mouseXWhenLastDragged = e.x; + mouseYWhenLastDragged = e.y; + lastAngle = rotaryStart + (rotaryEnd - rotaryStart) + * valueToProportionOfLength (currentValue); + + if (sliderBeingDragged == 2) + valueWhenLastDragged = valueMax; + else if (sliderBeingDragged == 1) + valueWhenLastDragged = valueMin; + else + valueWhenLastDragged = currentValue; + + valueOnMouseDown = valueWhenLastDragged; + + if (popupDisplayEnabled) + { + SliderPopupDisplayComponent* const popup = new SliderPopupDisplayComponent (this); + popupDisplay = popup; + + if (parentForPopupDisplay != 0) + { + parentForPopupDisplay->addChildComponent (popup); + } + else + { + popup->addToDesktop (0); + } + + popup->setVisible (true); + } + + sendDragStart(); + + mouseDrag (e); + } + } +} + +void Slider::mouseUp (const MouseEvent&) +{ + if (isEnabled() + && (! menuShown) + && (maximum > minimum) + && (style != IncDecButtons || incDecDragged)) + { + restoreMouseIfHidden(); + + if (sendChangeOnlyOnRelease && valueOnMouseDown != currentValue) + triggerChangeMessage (false); + + sendDragEnd(); + + deleteAndZero (popupDisplay); + + if (style == IncDecButtons) + { + incButton->setState (Button::buttonNormal); + decButton->setState (Button::buttonNormal); + } + } +} + +void Slider::restoreMouseIfHidden() +{ + if (mouseWasHidden) + { + mouseWasHidden = false; + + Component* c = Component::getComponentUnderMouse(); + + if (c == 0) + c = this; + + c->enableUnboundedMouseMovement (false); + + const double pos = (sliderBeingDragged == 2) ? getMaxValue() + : ((sliderBeingDragged == 1) ? getMinValue() + : currentValue); + + const int pixelPos = (int) getLinearSliderPos (pos); + + int x = isHorizontal() ? pixelPos : (getWidth() / 2); + int y = isVertical() ? pixelPos : (getHeight() / 2); + + relativePositionToGlobal (x, y); + Desktop::setMousePosition (x, y); + } +} + +void Slider::modifierKeysChanged (const ModifierKeys& modifiers) +{ + if (isEnabled() + && style != IncDecButtons + && style != Rotary + && isVelocityBased == modifiers.isAnyModifierKeyDown()) + { + restoreMouseIfHidden(); + } +} + +static double smallestAngleBetween (double a1, double a2) +{ + return jmin (fabs (a1 - a2), + fabs (a1 + double_Pi * 2.0 - a2), + fabs (a2 + double_Pi * 2.0 - a1)); +} + +void Slider::mouseDrag (const MouseEvent& e) +{ + if (isEnabled() + && (! menuShown) + && (maximum > minimum)) + { + if (style == Rotary) + { + int dx = e.x - sliderRect.getCentreX(); + int dy = e.y - sliderRect.getCentreY(); + + if (dx * dx + dy * dy > 25) + { + double angle = atan2 ((double) dx, (double) -dy); + while (angle < 0.0) + angle += double_Pi * 2.0; + + if (rotaryStop && ! e.mouseWasClicked()) + { + if (fabs (angle - lastAngle) > double_Pi) + { + if (angle >= lastAngle) + angle -= double_Pi * 2.0; + else + angle += double_Pi * 2.0; + } + + if (angle >= lastAngle) + angle = jmin (angle, (double) jmax (rotaryStart, rotaryEnd)); + else + angle = jmax (angle, (double) jmin (rotaryStart, rotaryEnd)); + } + else + { + while (angle < rotaryStart) + angle += double_Pi * 2.0; + + if (angle > rotaryEnd) + { + if (smallestAngleBetween (angle, rotaryStart) <= smallestAngleBetween (angle, rotaryEnd)) + angle = rotaryStart; + else + angle = rotaryEnd; + } + } + + const double proportion = (angle - rotaryStart) / (rotaryEnd - rotaryStart); + + valueWhenLastDragged = proportionOfLengthToValue (jlimit (0.0, 1.0, proportion)); + + lastAngle = angle; + } + } + else + { + if (style == LinearBar && e.mouseWasClicked() + && valueBox != 0 && valueBox->isEditable()) + return; + + if (style == IncDecButtons) + { + if (! incDecDragged) + incDecDragged = e.getDistanceFromDragStart() > 10 && ! e.mouseWasClicked(); + + if (! incDecDragged) + return; + } + + + if ((isVelocityBased == (userKeyOverridesVelocity ? e.mods.testFlags (ModifierKeys::ctrlModifier | ModifierKeys::commandModifier | ModifierKeys::altModifier) + : false)) + || ((maximum - minimum) / sliderRegionSize < interval)) + { + const int mousePos = (isHorizontal() || style == RotaryHorizontalDrag) ? e.x : e.y; + + double scaledMousePos = (mousePos - sliderRegionStart) / (double) sliderRegionSize; + + if (style == RotaryHorizontalDrag + || style == RotaryVerticalDrag + || style == IncDecButtons + || ((style == LinearHorizontal || style == LinearVertical || style == LinearBar) + && ! snapsToMousePos)) + { + const int mouseDiff = (style == RotaryHorizontalDrag + || style == LinearHorizontal + || style == LinearBar + || (style == IncDecButtons && incDecDragDirectionIsHorizontal())) + ? e.getDistanceFromDragStartX() + : -e.getDistanceFromDragStartY(); + + double newPos = valueToProportionOfLength (valueOnMouseDown) + + mouseDiff * (1.0 / pixelsForFullDragExtent); + + valueWhenLastDragged = proportionOfLengthToValue (jlimit (0.0, 1.0, newPos)); + + if (style == IncDecButtons) + { + incButton->setState (mouseDiff < 0 ? Button::buttonNormal : Button::buttonDown); + decButton->setState (mouseDiff > 0 ? Button::buttonNormal : Button::buttonDown); + } + } + else + { + if (style == LinearVertical) + scaledMousePos = 1.0 - scaledMousePos; + + valueWhenLastDragged = proportionOfLengthToValue (jlimit (0.0, 1.0, scaledMousePos)); + } + } + else + { + const int mouseDiff = (isHorizontal() || style == RotaryHorizontalDrag + || (style == IncDecButtons && incDecDragDirectionIsHorizontal())) + ? e.x - mouseXWhenLastDragged + : e.y - mouseYWhenLastDragged; + + const double maxSpeed = jmax (200, sliderRegionSize); + double speed = jlimit (0.0, maxSpeed, (double) abs (mouseDiff)); + + if (speed != 0) + { + speed = 0.2 * velocityModeSensitivity + * (1.0 + sin (double_Pi * (1.5 + jmin (0.5, velocityModeOffset + + jmax (0.0, (double) (speed - velocityModeThreshold)) + / maxSpeed)))); + + if (mouseDiff < 0) + speed = -speed; + + if (style == LinearVertical || style == RotaryVerticalDrag + || (style == IncDecButtons && ! incDecDragDirectionIsHorizontal())) + speed = -speed; + + const double currentPos = valueToProportionOfLength (valueWhenLastDragged); + + valueWhenLastDragged = proportionOfLengthToValue (jlimit (0.0, 1.0, currentPos + speed)); + + e.originalComponent->enableUnboundedMouseMovement (true, false); + mouseWasHidden = true; + } + } + } + + valueWhenLastDragged = jlimit (minimum, maximum, valueWhenLastDragged); + + if (sliderBeingDragged == 0) + { + setValue (snapValue (valueWhenLastDragged, true), + ! sendChangeOnlyOnRelease, true); + } + else if (sliderBeingDragged == 1) + { + setMinValue (snapValue (valueWhenLastDragged, true), + ! sendChangeOnlyOnRelease, false); + + if (e.mods.isShiftDown()) + setMaxValue (getMinValue() + minMaxDiff, false); + else + minMaxDiff = valueMax - valueMin; + } + else + { + jassert (sliderBeingDragged == 2); + + setMaxValue (snapValue (valueWhenLastDragged, true), + ! sendChangeOnlyOnRelease, false); + + if (e.mods.isShiftDown()) + setMinValue (getMaxValue() - minMaxDiff, false); + else + minMaxDiff = valueMax - valueMin; + } + + mouseXWhenLastDragged = e.x; + mouseYWhenLastDragged = e.y; + } +} + +void Slider::mouseDoubleClick (const MouseEvent&) +{ + if (doubleClickToValue + && isEnabled() + && style != IncDecButtons + && minimum <= doubleClickReturnValue + && maximum >= doubleClickReturnValue) + { + sendDragStart(); + setValue (doubleClickReturnValue, true, true); + sendDragEnd(); + } +} + +void Slider::mouseWheelMove (const MouseEvent& e, float wheelIncrementX, float wheelIncrementY) +{ + if (scrollWheelEnabled && isEnabled() + && style != TwoValueHorizontal + && style != TwoValueVertical) + { + if (maximum > minimum && ! isMouseButtonDownAnywhere()) + { + if (valueBox != 0) + valueBox->hideEditor (false); + + const double proportionDelta = (wheelIncrementX != 0 ? -wheelIncrementX : wheelIncrementY) * 0.15f; + const double currentPos = valueToProportionOfLength (currentValue); + const double newValue = proportionOfLengthToValue (jlimit (0.0, 1.0, currentPos + proportionDelta)); + + double delta = (newValue != currentValue) + ? jmax (fabs (newValue - currentValue), interval) : 0; + + if (currentValue > newValue) + delta = -delta; + + sendDragStart(); + setValue (snapValue (currentValue + delta, false), true, true); + sendDragEnd(); + } + } + else + { + Component::mouseWheelMove (e, wheelIncrementX, wheelIncrementY); + } +} + +void SliderListener::sliderDragStarted (Slider*) +{ +} + +void SliderListener::sliderDragEnded (Slider*) +{ +} + + +END_JUCE_NAMESPACE diff --git a/src/juce_appframework/gui/components/controls/juce_TextEditor.cpp b/src/juce_appframework/gui/components/controls/juce_TextEditor.cpp index 6d1d304020..f3fd326d70 100644 --- a/src/juce_appframework/gui/components/controls/juce_TextEditor.cpp +++ b/src/juce_appframework/gui/components/controls/juce_TextEditor.cpp @@ -1,2597 +1,2647 @@ -/* - ============================================================================== - - This file is part of the JUCE library - "Jules' Utility Class Extensions" - Copyright 2004-7 by Raw Material Software ltd. - - ------------------------------------------------------------------------------ - - JUCE can be redistributed and/or modified 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. - - JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the - Free Software Foundation, Inc., 59 Temple Place, Suite 330, - Boston, MA 02111-1307 USA - - ------------------------------------------------------------------------------ - - If you'd like to release a closed-source product which uses JUCE, commercial - licenses are also available: visit www.rawmaterialsoftware.com/juce for - more information. - - ============================================================================== -*/ - -#include "../../../../juce_core/basics/juce_StandardHeader.h" - -BEGIN_JUCE_NAMESPACE - -#include "juce_TextEditor.h" -#include "../../graphics/fonts/juce_GlyphArrangement.h" -#include "../../../application/juce_SystemClipboard.h" -#include "../../../../juce_core/basics/juce_Time.h" -#include "../../../../juce_core/text/juce_LocalisedStrings.h" -#include "../lookandfeel/juce_LookAndFeel.h" - -#define SHOULD_WRAP(x, wrapwidth) (((x) - 0.0001f) >= (wrapwidth)) - -//============================================================================== -// a word or space that can't be broken down any further -struct TextAtom -{ - //============================================================================== - String atomText; - float width; - uint16 numChars; - - //============================================================================== - bool isWhitespace() const throw() { return CharacterFunctions::isWhitespace (atomText[0]); } - bool isNewLine() const throw() { return atomText[0] == T('\r') || atomText[0] == T('\n'); } - - const String getText (const tchar passwordCharacter) const throw() - { - if (passwordCharacter == 0) - return atomText; - else - return String::repeatedString (String::charToString (passwordCharacter), - atomText.length()); - } - - const String getTrimmedText (const tchar passwordCharacter) const throw() - { - if (passwordCharacter == 0) - return atomText.substring (0, numChars); - else if (isNewLine()) - return String::empty; - else - return String::repeatedString (String::charToString (passwordCharacter), numChars); - } -}; - -//============================================================================== -// a run of text with a single font and colour -class UniformTextSection -{ -public: - //============================================================================== - UniformTextSection (const String& text, - const Font& font_, - const Colour& colour_, - const tchar passwordCharacter) throw() - : font (font_), - colour (colour_), - atoms (64) - { - initialiseAtoms (text, passwordCharacter); - } - - UniformTextSection (const UniformTextSection& other) throw() - : font (other.font), - colour (other.colour), - atoms (64) - { - for (int i = 0; i < other.atoms.size(); ++i) - atoms.add (new TextAtom (*(const TextAtom*) other.atoms.getUnchecked(i))); - } - - ~UniformTextSection() throw() - { - // (no need to delete the atoms, as they're explicitly deleted by the caller) - } - - void clear() throw() - { - for (int i = atoms.size(); --i >= 0;) - { - TextAtom* const atom = getAtom(i); - delete atom; - } - - atoms.clear(); - } - - int getNumAtoms() const throw() - { - return atoms.size(); - } - - TextAtom* getAtom (const int index) const throw() - { - return (TextAtom*) atoms.getUnchecked (index); - } - - void append (const UniformTextSection& other, const tchar passwordCharacter) throw() - { - if (other.atoms.size() > 0) - { - TextAtom* const lastAtom = (TextAtom*) atoms.getLast(); - int i = 0; - - if (lastAtom != 0) - { - if (! CharacterFunctions::isWhitespace (lastAtom->atomText.getLastCharacter())) - { - TextAtom* const first = other.getAtom(0); - - if (! CharacterFunctions::isWhitespace (first->atomText[0])) - { - lastAtom->atomText += first->atomText; - lastAtom->numChars = (uint16) (lastAtom->numChars + first->numChars); - lastAtom->width = font.getStringWidthFloat (lastAtom->getText (passwordCharacter)); - delete first; - ++i; - } - } - } - - while (i < other.atoms.size()) - { - atoms.add (other.getAtom(i)); - ++i; - } - } - } - - UniformTextSection* split (const int indexToBreakAt, - const tchar passwordCharacter) throw() - { - UniformTextSection* const section2 = new UniformTextSection (String::empty, - font, colour, - passwordCharacter); - int index = 0; - - for (int i = 0; i < atoms.size(); ++i) - { - TextAtom* const atom = getAtom(i); - - const int nextIndex = index + atom->numChars; - - if (index == indexToBreakAt) - { - int j; - for (j = i; j < atoms.size(); ++j) - section2->atoms.add (getAtom (j)); - - for (j = atoms.size(); --j >= i;) - atoms.remove (j); - - break; - } - else if (indexToBreakAt >= index && indexToBreakAt < nextIndex) - { - TextAtom* const secondAtom = new TextAtom(); - - secondAtom->atomText = atom->atomText.substring (indexToBreakAt - index); - secondAtom->width = font.getStringWidthFloat (secondAtom->getText (passwordCharacter)); - secondAtom->numChars = (uint16) secondAtom->atomText.length(); - - section2->atoms.add (secondAtom); - - atom->atomText = atom->atomText.substring (0, indexToBreakAt - index); - atom->width = font.getStringWidthFloat (atom->getText (passwordCharacter)); - atom->numChars = (uint16) (indexToBreakAt - index); - - int j; - for (j = i + 1; j < atoms.size(); ++j) - section2->atoms.add (getAtom (j)); - - for (j = atoms.size(); --j > i;) - atoms.remove (j); - - break; - } - - index = nextIndex; - } - - return section2; - } - - const String getAllText() const throw() - { - String s; - s.preallocateStorage (getTotalLength()); - - tchar* endOfString = (tchar*) &(s[0]); - - for (int i = 0; i < atoms.size(); ++i) - { - const TextAtom* const atom = getAtom(i); - - memcpy (endOfString, &(atom->atomText[0]), atom->numChars * sizeof (tchar)); - endOfString += atom->numChars; - } - - *endOfString = 0; - - jassert ((endOfString - (tchar*) &(s[0])) <= getTotalLength()); - return s; - } - - const String getTextSubstring (const int startCharacter, - const int endCharacter) const throw() - { - int index = 0; - int totalLen = 0; - int i; - - for (i = 0; i < atoms.size(); ++i) - { - const TextAtom* const atom = getAtom (i); - const int nextIndex = index + atom->numChars; - - if (startCharacter < nextIndex) - { - if (endCharacter <= index) - break; - - const int start = jmax (0, startCharacter - index); - const int end = jmin (endCharacter - index, atom->numChars); - jassert (end >= start); - - totalLen += end - start; - } - - index = nextIndex; - } - - String s; - s.preallocateStorage (totalLen + 1); - tchar* psz = (tchar*) (const tchar*) s; - - index = 0; - - for (i = 0; i < atoms.size(); ++i) - { - const TextAtom* const atom = getAtom (i); - const int nextIndex = index + atom->numChars; - - if (startCharacter < nextIndex) - { - if (endCharacter <= index) - break; - - const int start = jmax (0, startCharacter - index); - const int len = jmin (endCharacter - index, atom->numChars) - start; - - memcpy (psz, ((const tchar*) atom->atomText) + start, len * sizeof (tchar)); - psz += len; - *psz = 0; - } - - index = nextIndex; - } - - return s; - } - - int getTotalLength() const throw() - { - int c = 0; - - for (int i = atoms.size(); --i >= 0;) - c += getAtom(i)->numChars; - - return c; - } - - void setFont (const Font& newFont, - const tchar passwordCharacter) throw() - { - if (font != newFont) - { - font = newFont; - - for (int i = atoms.size(); --i >= 0;) - { - TextAtom* const atom = (TextAtom*) atoms.getUnchecked(i); - atom->width = newFont.getStringWidthFloat (atom->getText (passwordCharacter)); - } - } - } - - //============================================================================== - juce_UseDebuggingNewOperator - - Font font; - Colour colour; - -private: - VoidArray atoms; - - //============================================================================== - void initialiseAtoms (const String& textToParse, - const tchar passwordCharacter) throw() - { - int i = 0; - const int len = textToParse.length(); - const tchar* const text = (const tchar*) textToParse; - - while (i < len) - { - int start = i; - - // create a whitespace atom unless it starts with non-ws - if (CharacterFunctions::isWhitespace (text[i]) - && text[i] != T('\r') - && text[i] != T('\n')) - { - while (i < len - && CharacterFunctions::isWhitespace (text[i]) - && text[i] != T('\r') - && text[i] != T('\n')) - { - ++i; - } - } - else - { - if (text[i] == T('\r')) - { - ++i; - - if ((i < len) && (text[i] == T('\n'))) - { - ++start; - ++i; - } - } - else if (text[i] == T('\n')) - { - ++i; - } - else - { - while ((i < len) && ! CharacterFunctions::isWhitespace (text[i])) - ++i; - } - } - - TextAtom* const atom = new TextAtom(); - atom->atomText = String (text + start, i - start); - - atom->width = font.getStringWidthFloat (atom->getText (passwordCharacter)); - atom->numChars = (uint16) (i - start); - - atoms.add (atom); - } - } - - const UniformTextSection& operator= (const UniformTextSection& other); -}; - -//============================================================================== -class TextEditorIterator -{ -public: - //============================================================================== - TextEditorIterator (const VoidArray& sections_, - const float wordWrapWidth_, - const tchar passwordCharacter_) throw() - : indexInText (0), - lineY (0), - lineHeight (0), - maxDescent (0), - atomX (0), - atomRight (0), - atom (0), - currentSection (0), - sections (sections_), - sectionIndex (0), - atomIndex (0), - wordWrapWidth (wordWrapWidth_), - passwordCharacter (passwordCharacter_) - { - jassert (wordWrapWidth_ > 0); - - if (sections.size() > 0) - currentSection = (const UniformTextSection*) sections.getUnchecked (sectionIndex); - - if (currentSection != 0) - { - lineHeight = currentSection->font.getHeight(); - maxDescent = currentSection->font.getDescent(); - } - } - - TextEditorIterator (const TextEditorIterator& other) throw() - : indexInText (other.indexInText), - lineY (other.lineY), - lineHeight (other.lineHeight), - maxDescent (other.maxDescent), - atomX (other.atomX), - atomRight (other.atomRight), - atom (other.atom), - currentSection (other.currentSection), - sections (other.sections), - sectionIndex (other.sectionIndex), - atomIndex (other.atomIndex), - wordWrapWidth (other.wordWrapWidth), - passwordCharacter (other.passwordCharacter), - tempAtom (other.tempAtom) - { - } - - ~TextEditorIterator() throw() - { - } - - //============================================================================== - bool next() throw() - { - if (atom == &tempAtom) - { - const int numRemaining = tempAtom.atomText.length() - tempAtom.numChars; - - if (numRemaining > 0) - { - tempAtom.atomText = tempAtom.atomText.substring (tempAtom.numChars); - - atomX = 0; - - if (tempAtom.numChars > 0) - lineY += lineHeight; - - indexInText += tempAtom.numChars; - - GlyphArrangement g; - g.addLineOfText (currentSection->font, atom->getText (passwordCharacter), 0.0f, 0.0f); - - int split; - for (split = 0; split < g.getNumGlyphs(); ++split) - if (SHOULD_WRAP (g.getGlyph (split).getRight(), wordWrapWidth)) - break; - - if (split > 0 && split <= numRemaining) - { - tempAtom.numChars = (uint16) split; - tempAtom.width = g.getGlyph (split - 1).getRight(); - atomRight = atomX + tempAtom.width; - return true; - } - } - } - - bool forceNewLine = false; - - if (sectionIndex >= sections.size()) - { - moveToEndOfLastAtom(); - return false; - } - else if (atomIndex >= currentSection->getNumAtoms() - 1) - { - if (atomIndex >= currentSection->getNumAtoms()) - { - if (++sectionIndex >= sections.size()) - { - moveToEndOfLastAtom(); - return false; - } - - atomIndex = 0; - currentSection = (const UniformTextSection*) sections.getUnchecked (sectionIndex); - - lineHeight = jmax (lineHeight, currentSection->font.getHeight()); - maxDescent = jmax (maxDescent, currentSection->font.getDescent()); - } - else - { - const TextAtom* const lastAtom = currentSection->getAtom (atomIndex); - - if (! lastAtom->isWhitespace()) - { - // handle the case where the last atom in a section is actually part of the same - // word as the first atom of the next section... - float right = atomRight + lastAtom->width; - float lineHeight2 = lineHeight; - float maxDescent2 = maxDescent; - - for (int section = sectionIndex + 1; section < sections.size(); ++section) - { - const UniformTextSection* const s = (const UniformTextSection*) sections.getUnchecked (section); - - if (s->getNumAtoms() == 0) - break; - - const TextAtom* const nextAtom = s->getAtom (0); - - if (nextAtom->isWhitespace()) - break; - - right += nextAtom->width; - - lineHeight2 = jmax (lineHeight2, s->font.getHeight()); - maxDescent2 = jmax (maxDescent2, s->font.getDescent()); - - if (SHOULD_WRAP (right, wordWrapWidth)) - { - lineHeight = lineHeight2; - maxDescent = maxDescent2; - - forceNewLine = true; - break; - } - - if (s->getNumAtoms() > 1) - break; - } - } - } - } - - if (atom != 0) - { - atomX = atomRight; - indexInText += atom->numChars; - - if (atom->isNewLine()) - { - atomX = 0; - lineY += lineHeight; - } - } - - atom = currentSection->getAtom (atomIndex); - atomRight = atomX + atom->width; - ++atomIndex; - - if (SHOULD_WRAP (atomRight, wordWrapWidth) || forceNewLine) - { - if (atom->isWhitespace()) - { - // leave whitespace at the end of a line, but truncate it to avoid scrolling - atomRight = jmin (atomRight, wordWrapWidth); - } - else - { - return wrapCurrentAtom(); - } - } - - return true; - } - - bool wrapCurrentAtom() throw() - { - atomRight = atom->width; - - if (SHOULD_WRAP (atomRight, wordWrapWidth)) // atom too big to fit on a line, so break it up.. - { - tempAtom = *atom; - tempAtom.width = 0; - tempAtom.numChars = 0; - atom = &tempAtom; - - if (atomX > 0) - { - atomX = 0; - lineY += lineHeight; - } - - return next(); - } - - atomX = 0; - lineY += lineHeight; - return true; - } - - //============================================================================== - void draw (Graphics& g, const UniformTextSection*& lastSection) const throw() - { - if (passwordCharacter != 0 || ! atom->isWhitespace()) - { - if (lastSection != currentSection) - { - lastSection = currentSection; - g.setColour (currentSection->colour); - g.setFont (currentSection->font); - } - - jassert (atom->getTrimmedText (passwordCharacter).isNotEmpty()); - - GlyphArrangement ga; - ga.addLineOfText (currentSection->font, - atom->getTrimmedText (passwordCharacter), - atomX, - (float) roundFloatToInt (lineY + lineHeight - maxDescent)); - ga.draw (g); - } - } - - void drawSelection (Graphics& g, - const int selectionStart, - const int selectionEnd) const throw() - { - const int startX = roundFloatToInt (indexToX (selectionStart)); - const int endX = roundFloatToInt (indexToX (selectionEnd)); - - const int y = roundFloatToInt (lineY); - const int nextY = roundFloatToInt (lineY + lineHeight); - - g.fillRect (startX, y, endX - startX, nextY - y); - } - - void drawSelectedText (Graphics& g, - const int selectionStart, - const int selectionEnd, - const Colour& selectedTextColour) const throw() - { - if (passwordCharacter != 0 || ! atom->isWhitespace()) - { - GlyphArrangement ga; - ga.addLineOfText (currentSection->font, - atom->getTrimmedText (passwordCharacter), - atomX, - (float) roundFloatToInt (lineY + lineHeight - maxDescent)); - - if (selectionEnd < indexInText + atom->numChars) - { - GlyphArrangement ga2 (ga); - ga2.removeRangeOfGlyphs (0, selectionEnd - indexInText); - ga.removeRangeOfGlyphs (selectionEnd - indexInText, -1); - - g.setColour (currentSection->colour); - ga2.draw (g); - } - - if (selectionStart > indexInText) - { - GlyphArrangement ga2 (ga); - ga2.removeRangeOfGlyphs (selectionStart - indexInText, -1); - ga.removeRangeOfGlyphs (0, selectionStart - indexInText); - - g.setColour (currentSection->colour); - ga2.draw (g); - } - - g.setColour (selectedTextColour); - ga.draw (g); - } - } - - //============================================================================== - float indexToX (const int indexToFind) const throw() - { - if (indexToFind <= indexInText) - return atomX; - - if (indexToFind >= indexInText + atom->numChars) - return atomRight; - - GlyphArrangement g; - g.addLineOfText (currentSection->font, - atom->getText (passwordCharacter), - atomX, 0.0f); - - return jmin (atomRight, g.getGlyph (indexToFind - indexInText).getLeft()); - } - - int xToIndex (const float xToFind) const throw() - { - if (xToFind <= atomX || atom->isNewLine()) - return indexInText; - - if (xToFind >= atomRight) - return indexInText + atom->numChars; - - GlyphArrangement g; - g.addLineOfText (currentSection->font, - atom->getText (passwordCharacter), - atomX, 0.0f); - - int j; - for (j = 0; j < atom->numChars; ++j) - if ((g.getGlyph(j).getLeft() + g.getGlyph(j).getRight()) / 2 > xToFind) - break; - - return indexInText + j; - } - - //============================================================================== - void updateLineHeight() throw() - { - float x = atomRight; - - int tempSectionIndex = sectionIndex; - int tempAtomIndex = atomIndex; - const UniformTextSection* currentSection = (const UniformTextSection*) sections.getUnchecked (tempSectionIndex); - - while (! SHOULD_WRAP (x, wordWrapWidth)) - { - if (tempSectionIndex >= sections.size()) - break; - - bool checkSize = false; - - if (tempAtomIndex >= currentSection->getNumAtoms()) - { - if (++tempSectionIndex >= sections.size()) - break; - - tempAtomIndex = 0; - currentSection = (const UniformTextSection*) sections.getUnchecked (tempSectionIndex); - checkSize = true; - } - - const TextAtom* const atom = currentSection->getAtom (tempAtomIndex); - - if (atom == 0) - break; - - x += atom->width; - - if (SHOULD_WRAP (x, wordWrapWidth) || atom->isNewLine()) - break; - - if (checkSize) - { - lineHeight = jmax (lineHeight, currentSection->font.getHeight()); - maxDescent = jmax (maxDescent, currentSection->font.getDescent()); - } - - ++tempAtomIndex; - } - } - - bool getCharPosition (const int index, float& cx, float& cy, float& lineHeight_) throw() - { - while (next()) - { - if (indexInText + atom->numChars >= index) - { - updateLineHeight(); - - if (indexInText + atom->numChars > index) - { - cx = indexToX (index); - cy = lineY; - lineHeight_ = lineHeight; - return true; - } - } - } - - cx = atomX; - cy = lineY; - lineHeight_ = lineHeight; - return false; - } - - //============================================================================== - juce_UseDebuggingNewOperator - - int indexInText; - float lineY, lineHeight, maxDescent; - float atomX, atomRight; - const TextAtom* atom; - const UniformTextSection* currentSection; - -private: - const VoidArray& sections; - int sectionIndex, atomIndex; - const float wordWrapWidth; - const tchar passwordCharacter; - TextAtom tempAtom; - - const TextEditorIterator& operator= (const TextEditorIterator&); - - void moveToEndOfLastAtom() throw() - { - if (atom != 0) - { - atomX = atomRight; - - if (atom->isNewLine()) - { - atomX = 0.0f; - lineY += lineHeight; - } - } - } -}; - - -//============================================================================== -class TextEditorInsertAction : public UndoableAction -{ - TextEditor& owner; - const String text; - const int insertIndex, oldCaretPos, newCaretPos; - const Font font; - const Colour colour; - - TextEditorInsertAction (const TextEditorInsertAction&); - const TextEditorInsertAction& operator= (const TextEditorInsertAction&); - -public: - TextEditorInsertAction (TextEditor& owner_, - const String& text_, - const int insertIndex_, - const Font& font_, - const Colour& colour_, - const int oldCaretPos_, - const int newCaretPos_) throw() - : owner (owner_), - text (text_), - insertIndex (insertIndex_), - oldCaretPos (oldCaretPos_), - newCaretPos (newCaretPos_), - font (font_), - colour (colour_) - { - } - - ~TextEditorInsertAction() - { - } - - bool perform() - { - owner.insert (text, insertIndex, font, colour, 0, newCaretPos); - return true; - } - - bool undo() - { - owner.remove (insertIndex, insertIndex + text.length(), 0, oldCaretPos); - return true; - } - - int getSizeInUnits() - { - return text.length() + 16; - } -}; - -//============================================================================== -class TextEditorRemoveAction : public UndoableAction -{ - TextEditor& owner; - const int startIndex, endIndex, oldCaretPos, newCaretPos; - VoidArray removedSections; - - TextEditorRemoveAction (const TextEditorRemoveAction&); - const TextEditorRemoveAction& operator= (const TextEditorRemoveAction&); - -public: - TextEditorRemoveAction (TextEditor& owner_, - const int startIndex_, - const int endIndex_, - const int oldCaretPos_, - const int newCaretPos_, - const VoidArray& removedSections_) throw() - : owner (owner_), - startIndex (startIndex_), - endIndex (endIndex_), - oldCaretPos (oldCaretPos_), - newCaretPos (newCaretPos_), - removedSections (removedSections_) - { - } - - ~TextEditorRemoveAction() - { - for (int i = removedSections.size(); --i >= 0;) - { - UniformTextSection* const section = (UniformTextSection*) removedSections.getUnchecked (i); - section->clear(); - delete section; - } - } - - bool perform() - { - owner.remove (startIndex, endIndex, 0, newCaretPos); - return true; - } - - bool undo() - { - owner.reinsert (startIndex, removedSections); - owner.moveCursorTo (oldCaretPos, false); - return true; - } - - int getSizeInUnits() - { - int n = 0; - - for (int i = removedSections.size(); --i >= 0;) - { - UniformTextSection* const section = (UniformTextSection*) removedSections.getUnchecked (i); - n += section->getTotalLength(); - } - - return n + 16; - } -}; - -//============================================================================== -class TextHolderComponent : public Component, - public Timer -{ - TextEditor* const owner; - - TextHolderComponent (const TextHolderComponent&); - const TextHolderComponent& operator= (const TextHolderComponent&); - -public: - TextHolderComponent (TextEditor* const owner_) - : owner (owner_) - { - setWantsKeyboardFocus (false); - setInterceptsMouseClicks (false, true); - } - - ~TextHolderComponent() - { - } - - void paint (Graphics& g) - { - owner->drawContent (g); - } - - void timerCallback() - { - owner->timerCallbackInt(); - } - - const MouseCursor getMouseCursor() - { - return owner->getMouseCursor(); - } -}; - -//============================================================================== -class TextEditorViewport : public Viewport -{ - TextEditor* const owner; - float lastWordWrapWidth; - - TextEditorViewport (const TextEditorViewport&); - const TextEditorViewport& operator= (const TextEditorViewport&); - -public: - TextEditorViewport (TextEditor* const owner_) - : owner (owner_), - lastWordWrapWidth (0) - { - } - - ~TextEditorViewport() - { - } - - void visibleAreaChanged (int, int, int, int) - { - const float wordWrapWidth = owner->getWordWrapWidth(); - - if (wordWrapWidth != lastWordWrapWidth) - { - lastWordWrapWidth = wordWrapWidth; - owner->updateTextHolderSize(); - } - } -}; - -//============================================================================== -const int flashSpeedIntervalMs = 380; - -const int textChangeMessageId = 0x10003001; -const int returnKeyMessageId = 0x10003002; -const int escapeKeyMessageId = 0x10003003; -const int focusLossMessageId = 0x10003004; - - -//============================================================================== -TextEditor::TextEditor (const String& name, - const tchar passwordCharacter_) - : Component (name), - borderSize (1, 1, 1, 3), - readOnly (false), - multiline (false), - wordWrap (false), - returnKeyStartsNewLine (false), - caretVisible (true), - popupMenuEnabled (true), - selectAllTextWhenFocused (false), - scrollbarVisible (true), - wasFocused (false), - caretFlashState (true), - keepCursorOnScreen (true), - tabKeyUsed (false), - menuActive (false), - cursorX (0), - cursorY (0), - cursorHeight (0), - maxTextLength (0), - selectionStart (0), - selectionEnd (0), - leftIndent (4), - topIndent (4), - lastTransactionTime (0), - currentFont (14.0f), - totalNumChars (0), - caretPosition (0), - sections (8), - passwordCharacter (passwordCharacter_), - dragType (notDragging), - listeners (2) -{ - setOpaque (true); - - addAndMakeVisible (viewport = new TextEditorViewport (this)); - viewport->setViewedComponent (textHolder = new TextHolderComponent (this)); - viewport->setWantsKeyboardFocus (false); - viewport->setScrollBarsShown (false, false); - - setMouseCursor (MouseCursor::IBeamCursor); - setWantsKeyboardFocus (true); -} - -TextEditor::~TextEditor() -{ - clearInternal (0); - delete viewport; -} - -//============================================================================== -void TextEditor::newTransaction() throw() -{ - lastTransactionTime = Time::getApproximateMillisecondCounter(); - undoManager.beginNewTransaction(); -} - -void TextEditor::doUndoRedo (const bool isRedo) -{ - if (! isReadOnly()) - { - if ((isRedo) ? undoManager.redo() - : undoManager.undo()) - { - scrollToMakeSureCursorIsVisible(); - repaint(); - textChanged(); - } - } -} - -//============================================================================== -void TextEditor::setMultiLine (const bool shouldBeMultiLine, - const bool shouldWordWrap) -{ - multiline = shouldBeMultiLine; - wordWrap = shouldWordWrap && shouldBeMultiLine; - - setScrollbarsShown (scrollbarVisible); - - viewport->setViewPosition (0, 0); - - resized(); - scrollToMakeSureCursorIsVisible(); -} - -bool TextEditor::isMultiLine() const throw() -{ - return multiline; -} - -void TextEditor::setScrollbarsShown (bool enabled) throw() -{ - scrollbarVisible = enabled; - - enabled = enabled && isMultiLine(); - - viewport->setScrollBarsShown (enabled, enabled); -} - -void TextEditor::setReadOnly (const bool shouldBeReadOnly) -{ - readOnly = shouldBeReadOnly; - enablementChanged(); -} - -bool TextEditor::isReadOnly() const throw() -{ - return readOnly || ! isEnabled(); -} - -void TextEditor::setReturnKeyStartsNewLine (const bool shouldStartNewLine) -{ - returnKeyStartsNewLine = shouldStartNewLine; -} - -void TextEditor::setTabKeyUsedAsCharacter (const bool shouldTabKeyBeUsed) throw() -{ - tabKeyUsed = shouldTabKeyBeUsed; -} - -void TextEditor::setPopupMenuEnabled (const bool b) throw() -{ - popupMenuEnabled = b; -} - -void TextEditor::setSelectAllWhenFocused (const bool b) throw() -{ - selectAllTextWhenFocused = b; -} - -//============================================================================== -const Font TextEditor::getFont() const throw() -{ - return currentFont; -} - -void TextEditor::setFont (const Font& newFont) throw() -{ - currentFont = newFont; - scrollToMakeSureCursorIsVisible(); -} - -void TextEditor::applyFontToAllText (const Font& newFont) -{ - currentFont = newFont; - - const Colour overallColour (findColour (textColourId)); - - for (int i = sections.size(); --i >= 0;) - { - UniformTextSection* const uts = (UniformTextSection*) sections.getUnchecked(i); - uts->setFont (newFont, passwordCharacter); - uts->colour = overallColour; - } - - coalesceSimilarSections(); - updateTextHolderSize(); - scrollToMakeSureCursorIsVisible(); - repaint(); -} - -void TextEditor::colourChanged() -{ - setOpaque (findColour (backgroundColourId).isOpaque()); - repaint(); -} - -void TextEditor::setCaretVisible (const bool shouldCaretBeVisible) throw() -{ - caretVisible = shouldCaretBeVisible; - - if (shouldCaretBeVisible) - textHolder->startTimer (flashSpeedIntervalMs); - - setMouseCursor (shouldCaretBeVisible ? MouseCursor::IBeamCursor - : MouseCursor::NormalCursor); -} - -void TextEditor::setInputRestrictions (const int maxLen, - const String& chars) throw() -{ - maxTextLength = jmax (0, maxLen); - allowedCharacters = chars; -} - -void TextEditor::setTextToShowWhenEmpty (const String& text, const Colour& colourToUse) throw() -{ - textToShowWhenEmpty = text; - colourForTextWhenEmpty = colourToUse; -} - -void TextEditor::setPasswordCharacter (const tchar newPasswordCharacter) throw() -{ - if (passwordCharacter != newPasswordCharacter) - { - passwordCharacter = newPasswordCharacter; - resized(); - repaint(); - } -} - -void TextEditor::setScrollBarThickness (const int newThicknessPixels) -{ - viewport->setScrollBarThickness (newThicknessPixels); -} - -void TextEditor::setScrollBarButtonVisibility (const bool buttonsVisible) -{ - viewport->setScrollBarButtonVisibility (buttonsVisible); -} - -//============================================================================== -void TextEditor::clear() -{ - clearInternal (0); - updateTextHolderSize(); - undoManager.clearUndoHistory(); -} - -void TextEditor::setText (const String& newText, - const bool sendTextChangeMessage) -{ - const int newLength = newText.length(); - - if (newLength != getTotalNumChars() || getText() != newText) - { - const int oldCursorPos = caretPosition; - const bool cursorWasAtEnd = oldCursorPos >= getTotalNumChars(); - - clearInternal (0); - insert (newText, 0, currentFont, findColour (textColourId), 0, caretPosition); - - // if you're adding text with line-feeds to a single-line text editor, it - // ain't gonna look right! - jassert (multiline || ! newText.containsAnyOf (T("\r\n"))); - - if (cursorWasAtEnd && ! isMultiLine()) - moveCursorTo (getTotalNumChars(), false); - else - moveCursorTo (oldCursorPos, false); - - if (sendTextChangeMessage) - textChanged(); - - repaint(); - } - - updateTextHolderSize(); - scrollToMakeSureCursorIsVisible(); - undoManager.clearUndoHistory(); -} - -//============================================================================== -void TextEditor::textChanged() throw() -{ - updateTextHolderSize(); - postCommandMessage (textChangeMessageId); -} - -void TextEditor::returnPressed() -{ - postCommandMessage (returnKeyMessageId); -} - -void TextEditor::escapePressed() -{ - postCommandMessage (escapeKeyMessageId); -} - -void TextEditor::addListener (TextEditorListener* const newListener) throw() -{ - jassert (newListener != 0) - - if (newListener != 0) - listeners.add (newListener); -} - -void TextEditor::removeListener (TextEditorListener* const listenerToRemove) throw() -{ - listeners.removeValue (listenerToRemove); -} - -//============================================================================== -void TextEditor::timerCallbackInt() -{ - const bool newState = (! caretFlashState) && ! isCurrentlyBlockedByAnotherModalComponent(); - - if (caretFlashState != newState) - { - caretFlashState = newState; - - if (caretFlashState) - wasFocused = true; - - if (caretVisible - && hasKeyboardFocus (false) - && ! isReadOnly()) - { - repaintCaret(); - } - } - - const unsigned int now = Time::getApproximateMillisecondCounter(); - - if (now > lastTransactionTime + 200) - newTransaction(); -} - -void TextEditor::repaintCaret() -{ - if (! findColour (caretColourId).isTransparent()) - repaint (borderSize.getLeft() + textHolder->getX() + leftIndent + roundFloatToInt (cursorX) - 1, - borderSize.getTop() + textHolder->getY() + topIndent + roundFloatToInt (cursorY) - 1, - 4, - roundFloatToInt (cursorHeight) + 2); -} - -void TextEditor::repaintText (int textStartIndex, int textEndIndex) -{ - if (textStartIndex > textEndIndex && textEndIndex > 0) - swapVariables (textStartIndex, textEndIndex); - - float x = 0, y = 0, lh = currentFont.getHeight(); - - const float wordWrapWidth = getWordWrapWidth(); - - if (wordWrapWidth > 0) - { - TextEditorIterator i (sections, wordWrapWidth, passwordCharacter); - - i.getCharPosition (textStartIndex, x, y, lh); - - const int y1 = (int) y; - int y2; - - if (textEndIndex >= 0) - { - i.getCharPosition (textEndIndex, x, y, lh); - y2 = (int) (y + lh * 2.0f); - } - else - { - y2 = textHolder->getHeight(); - } - - textHolder->repaint (0, y1, textHolder->getWidth(), y2 - y1); - } -} - -//============================================================================== -void TextEditor::moveCaret (int newCaretPos) throw() -{ - if (newCaretPos < 0) - newCaretPos = 0; - else if (newCaretPos > getTotalNumChars()) - newCaretPos = getTotalNumChars(); - - if (newCaretPos != getCaretPosition()) - { - repaintCaret(); - caretFlashState = true; - caretPosition = newCaretPos; - textHolder->startTimer (flashSpeedIntervalMs); - scrollToMakeSureCursorIsVisible(); - repaintCaret(); - } -} - -void TextEditor::setCaretPosition (const int newIndex) throw() -{ - moveCursorTo (newIndex, false); -} - -int TextEditor::getCaretPosition() const throw() -{ - return caretPosition; -} - -//============================================================================== -float TextEditor::getWordWrapWidth() const throw() -{ - return (wordWrap) ? (float) (viewport->getMaximumVisibleWidth() - leftIndent - leftIndent / 2) - : 1.0e10f; -} - -void TextEditor::updateTextHolderSize() throw() -{ - const float wordWrapWidth = getWordWrapWidth(); - - if (wordWrapWidth > 0) - { - float maxWidth = 0.0f; - - TextEditorIterator i (sections, wordWrapWidth, passwordCharacter); - - while (i.next()) - maxWidth = jmax (maxWidth, i.atomRight); - - const int w = leftIndent + roundFloatToInt (maxWidth); - const int h = topIndent + roundFloatToInt (jmax (i.lineY + i.lineHeight, - currentFont.getHeight())); - - textHolder->setSize (w + 1, h + 1); - } -} - -int TextEditor::getTextWidth() const throw() -{ - return textHolder->getWidth(); -} - -int TextEditor::getTextHeight() const throw() -{ - return textHolder->getHeight(); -} - -void TextEditor::setIndents (const int newLeftIndent, - const int newTopIndent) throw() -{ - leftIndent = newLeftIndent; - topIndent = newTopIndent; -} - -void TextEditor::setBorder (const BorderSize& border) throw() -{ - borderSize = border; - resized(); -} - -const BorderSize TextEditor::getBorder() const throw() -{ - return borderSize; -} - -void TextEditor::setScrollToShowCursor (const bool shouldScrollToShowCursor) throw() -{ - keepCursorOnScreen = shouldScrollToShowCursor; -} - -void TextEditor::scrollToMakeSureCursorIsVisible() throw() -{ - cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value) - - getCharPosition (caretPosition, - cursorX, cursorY, - cursorHeight); - - if (keepCursorOnScreen) - { - int x = viewport->getViewPositionX(); - int y = viewport->getViewPositionY(); - - const int relativeCursorX = roundFloatToInt (cursorX) - x; - const int relativeCursorY = roundFloatToInt (cursorY) - y; - - if (relativeCursorX < jmax (1, proportionOfWidth (0.05f))) - { - x += relativeCursorX - proportionOfWidth (0.2f); - } - else if (relativeCursorX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10))) - { - x += relativeCursorX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth(); - } - - x = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), x); - - if (! isMultiLine()) - { - y = (getHeight() - textHolder->getHeight() - topIndent) / -2; - } - else - { - const int curH = roundFloatToInt (cursorHeight); - - if (relativeCursorY < 0) - { - y = jmax (0, relativeCursorY + y); - } - else if (relativeCursorY > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - curH)) - { - y += relativeCursorY + 2 + curH + topIndent - viewport->getMaximumVisibleHeight(); - } - } - - viewport->setViewPosition (x, y); - } -} - -void TextEditor::moveCursorTo (const int newPosition, - const bool isSelecting) throw() -{ - if (isSelecting) - { - moveCaret (newPosition); - - const int oldSelStart = selectionStart; - const int oldSelEnd = selectionEnd; - - if (dragType == notDragging) - { - if (abs (getCaretPosition() - selectionStart) < abs (getCaretPosition() - selectionEnd)) - dragType = draggingSelectionStart; - else - dragType = draggingSelectionEnd; - } - - if (dragType == draggingSelectionStart) - { - selectionStart = getCaretPosition(); - - if (selectionEnd < selectionStart) - { - swapVariables (selectionStart, selectionEnd); - dragType = draggingSelectionEnd; - } - } - else - { - selectionEnd = getCaretPosition(); - - if (selectionEnd < selectionStart) - { - swapVariables (selectionStart, selectionEnd); - dragType = draggingSelectionStart; - } - } - - jassert (selectionStart <= selectionEnd); - jassert (oldSelStart <= oldSelEnd); - - repaintText (jmin (oldSelStart, selectionStart), - jmax (oldSelEnd, selectionEnd)); - } - else - { - dragType = notDragging; - - if (selectionEnd > selectionStart) - repaintText (selectionStart, selectionEnd); - - moveCaret (newPosition); - selectionStart = getCaretPosition(); - selectionEnd = getCaretPosition(); - } -} - -int TextEditor::getTextIndexAt (const int x, - const int y) throw() -{ - return indexAtPosition ((float) (x + viewport->getViewPositionX() - leftIndent), - (float) (y + viewport->getViewPositionY() - topIndent)); -} - -void TextEditor::insertTextAtCursor (String newText) -{ - if (allowedCharacters.isNotEmpty()) - newText = newText.retainCharacters (allowedCharacters); - - if (! isMultiLine()) - newText = newText.replaceCharacters (T("\r\n"), T(" ")); - else - newText = newText.replace (T("\r\n"), T("\n")); - - const int newCaretPos = selectionStart + newText.length(); - const int insertIndex = selectionStart; - - remove (selectionStart, selectionEnd, - &undoManager, - newCaretPos); - - if (maxTextLength > 0) - newText = newText.substring (0, maxTextLength - getTotalNumChars()); - - if (newText.isNotEmpty()) - insert (newText, - insertIndex, - currentFont, - findColour (textColourId), - &undoManager, - newCaretPos); - - textChanged(); -} - -void TextEditor::setHighlightedRegion (int startPos, int numChars) throw() -{ - moveCursorTo (startPos, false); - moveCursorTo (startPos + numChars, true); -} - -//============================================================================== -void TextEditor::copy() -{ - const String selection (getTextSubstring (selectionStart, selectionEnd)); - - if (selection.isNotEmpty()) - SystemClipboard::copyTextToClipboard (selection); -} - -void TextEditor::paste() -{ - if (! isReadOnly()) - { - const String clip (SystemClipboard::getTextFromClipboard()); - - if (clip.isNotEmpty()) - insertTextAtCursor (clip); - } -} - -void TextEditor::cut() -{ - if (! isReadOnly()) - { - moveCaret (selectionEnd); - insertTextAtCursor (String::empty); - } -} - -//============================================================================== -void TextEditor::drawContent (Graphics& g) -{ - const float wordWrapWidth = getWordWrapWidth(); - - if (wordWrapWidth > 0) - { - g.setOrigin (leftIndent, topIndent); - const Rectangle clip (g.getClipBounds()); - Colour selectedTextColour; - - TextEditorIterator i (sections, wordWrapWidth, passwordCharacter); - - while (i.lineY + 200.0 < clip.getY() && i.next()) - {} - - if (selectionStart < selectionEnd) - { - g.setColour (findColour (highlightColourId) - .withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f)); - - selectedTextColour = findColour (highlightedTextColourId); - - TextEditorIterator i2 (i); - - while (i2.next() && i2.lineY < clip.getBottom()) - { - i2.updateLineHeight(); - - if (i2.lineY + i2.lineHeight >= clip.getY() - && selectionEnd >= i2.indexInText - && selectionStart <= i2.indexInText + i2.atom->numChars) - { - i2.drawSelection (g, selectionStart, selectionEnd); - } - } - } - - const UniformTextSection* lastSection = 0; - - while (i.next() && i.lineY < clip.getBottom()) - { - i.updateLineHeight(); - - if (i.lineY + i.lineHeight >= clip.getY()) - { - if (selectionEnd >= i.indexInText - && selectionStart <= i.indexInText + i.atom->numChars) - { - i.drawSelectedText (g, selectionStart, selectionEnd, selectedTextColour); - lastSection = 0; - } - else - { - i.draw (g, lastSection); - } - } - } - } -} - -void TextEditor::paint (Graphics& g) -{ - getLookAndFeel().fillTextEditorBackground (g, getWidth(), getHeight(), *this); -} - -void TextEditor::paintOverChildren (Graphics& g) -{ - if (caretFlashState - && hasKeyboardFocus (false) - && caretVisible - && ! isReadOnly()) - { - g.setColour (findColour (caretColourId)); - - g.fillRect (borderSize.getLeft() + textHolder->getX() + leftIndent + cursorX, - borderSize.getTop() + textHolder->getY() + topIndent + cursorY, - 2.0f, cursorHeight); - } - - if (textToShowWhenEmpty.isNotEmpty() - && (! hasKeyboardFocus (false)) - && getTotalNumChars() == 0) - { - g.setColour (colourForTextWhenEmpty); - g.setFont (getFont()); - - if (isMultiLine()) - { - g.drawText (textToShowWhenEmpty, - 0, 0, getWidth(), getHeight(), - Justification::centred, true); - } - else - { - g.drawText (textToShowWhenEmpty, - leftIndent, topIndent, - viewport->getWidth() - leftIndent, - viewport->getHeight() - topIndent, - Justification::centredLeft, true); - } - } - - getLookAndFeel().drawTextEditorOutline (g, getWidth(), getHeight(), *this); -} - -//============================================================================== -void TextEditor::mouseDown (const MouseEvent& e) -{ - beginDragAutoRepeat (100); - newTransaction(); - - if (wasFocused || ! selectAllTextWhenFocused) - { - if (! (popupMenuEnabled && e.mods.isPopupMenu())) - { - moveCursorTo (getTextIndexAt (e.x, e.y), - e.mods.isShiftDown()); - } - else - { - PopupMenu m; - addPopupMenuItems (m, &e); - - menuActive = true; - const int result = m.show(); - menuActive = false; - - if (result != 0) - performPopupMenuAction (result); - } - } -} - -void TextEditor::mouseDrag (const MouseEvent& e) -{ - if (wasFocused || ! selectAllTextWhenFocused) - { - if (! (popupMenuEnabled && e.mods.isPopupMenu())) - { - moveCursorTo (getTextIndexAt (e.x, e.y), true); - } - } -} - -void TextEditor::mouseUp (const MouseEvent& e) -{ - newTransaction(); - textHolder->startTimer (flashSpeedIntervalMs); - - if (wasFocused || ! selectAllTextWhenFocused) - { - if (! (popupMenuEnabled && e.mods.isPopupMenu())) - { - moveCaret (getTextIndexAt (e.x, e.y)); - } - } - - wasFocused = true; -} - -void TextEditor::mouseDoubleClick (const MouseEvent& e) -{ - int tokenEnd = getTextIndexAt (e.x, e.y); - int tokenStart = tokenEnd; - - if (e.getNumberOfClicks() > 3) - { - tokenStart = 0; - tokenEnd = getTotalNumChars(); - } - else - { - const String t (getText()); - const int totalLength = getTotalNumChars(); - - while (tokenEnd < totalLength) - { - if (CharacterFunctions::isLetterOrDigit (t [tokenEnd])) - ++tokenEnd; - else - break; - } - - tokenStart = tokenEnd; - - while (tokenStart > 0) - { - if (CharacterFunctions::isLetterOrDigit (t [tokenStart - 1])) - --tokenStart; - else - break; - } - - if (e.getNumberOfClicks() > 2) - { - while (tokenEnd < totalLength) - { - if (t [tokenEnd] != T('\r') && t [tokenEnd] != T('\n')) - ++tokenEnd; - else - break; - } - - while (tokenStart > 0) - { - if (t [tokenStart - 1] != T('\r') && t [tokenStart - 1] != T('\n')) - --tokenStart; - else - break; - } - } - } - - moveCursorTo (tokenEnd, false); - moveCursorTo (tokenStart, true); -} - -void TextEditor::mouseWheelMove (const MouseEvent& e, float wheelIncrementX, float wheelIncrementY) -{ - if (! viewport->useMouseWheelMoveIfNeeded (e, wheelIncrementX, wheelIncrementY)) - Component::mouseWheelMove (e, wheelIncrementX, wheelIncrementY); -} - -//============================================================================== -bool TextEditor::keyPressed (const KeyPress& key) -{ - if (isReadOnly() && key != KeyPress (T('c'), ModifierKeys::commandModifier, 0)) - return false; - - const bool moveInWholeWordSteps = key.getModifiers().isCtrlDown() || key.getModifiers().isAltDown(); - - if (key.isKeyCode (KeyPress::leftKey) - || key.isKeyCode (KeyPress::upKey)) - { - newTransaction(); - - int newPos; - - if (isMultiLine() && key.isKeyCode (KeyPress::upKey)) - newPos = indexAtPosition (cursorX, cursorY - 1); - else if (moveInWholeWordSteps) - newPos = findWordBreakBefore (getCaretPosition()); - else - newPos = getCaretPosition() - 1; - - moveCursorTo (newPos, key.getModifiers().isShiftDown()); - } - else if (key.isKeyCode (KeyPress::rightKey) - || key.isKeyCode (KeyPress::downKey)) - { - newTransaction(); - - int newPos; - - if (isMultiLine() && key.isKeyCode (KeyPress::downKey)) - newPos = indexAtPosition (cursorX, cursorY + cursorHeight + 1); - else if (moveInWholeWordSteps) - newPos = findWordBreakAfter (getCaretPosition()); - else - newPos = getCaretPosition() + 1; - - moveCursorTo (newPos, key.getModifiers().isShiftDown()); - } - else if (key.isKeyCode (KeyPress::pageDownKey) && isMultiLine()) - { - newTransaction(); - - moveCursorTo (indexAtPosition (cursorX, cursorY + cursorHeight + viewport->getViewHeight()), - key.getModifiers().isShiftDown()); - } - else if (key.isKeyCode (KeyPress::pageUpKey) && isMultiLine()) - { - newTransaction(); - - moveCursorTo (indexAtPosition (cursorX, cursorY - viewport->getViewHeight()), - key.getModifiers().isShiftDown()); - } - else if (key.isKeyCode (KeyPress::homeKey)) - { - newTransaction(); - - if (isMultiLine() && ! moveInWholeWordSteps) - moveCursorTo (indexAtPosition (0.0f, cursorY), - key.getModifiers().isShiftDown()); - else - moveCursorTo (0, key.getModifiers().isShiftDown()); - } - else if (key.isKeyCode (KeyPress::endKey)) - { - newTransaction(); - - if (isMultiLine() && ! moveInWholeWordSteps) - moveCursorTo (indexAtPosition ((float) textHolder->getWidth(), cursorY), - key.getModifiers().isShiftDown()); - else - moveCursorTo (getTotalNumChars(), key.getModifiers().isShiftDown()); - } - else if (key.isKeyCode (KeyPress::backspaceKey)) - { - if (moveInWholeWordSteps) - { - moveCursorTo (findWordBreakBefore (getCaretPosition()), true); - } - else - { - if (selectionStart == selectionEnd && selectionStart > 0) - --selectionStart; - } - - cut(); - } - else if (key.isKeyCode (KeyPress::deleteKey)) - { - if (key.getModifiers().isShiftDown()) - copy(); - - if (selectionStart == selectionEnd - && selectionEnd < getTotalNumChars()) - { - ++selectionEnd; - } - - cut(); - } - else if (key == KeyPress (T('c'), ModifierKeys::commandModifier, 0) - || key == KeyPress (KeyPress::insertKey, ModifierKeys::ctrlModifier, 0)) - { - newTransaction(); - copy(); - } - else if (key == KeyPress (T('x'), ModifierKeys::commandModifier, 0)) - { - newTransaction(); - copy(); - cut(); - } - else if (key == KeyPress (T('v'), ModifierKeys::commandModifier, 0) - || key == KeyPress (KeyPress::insertKey, ModifierKeys::shiftModifier, 0)) - { - newTransaction(); - paste(); - } - else if (key == KeyPress (T('z'), ModifierKeys::commandModifier, 0)) - { - newTransaction(); - doUndoRedo (false); - } - else if (key == KeyPress (T('y'), ModifierKeys::commandModifier, 0)) - { - newTransaction(); - doUndoRedo (true); - } - else if (key == KeyPress (T('a'), ModifierKeys::commandModifier, 0)) - { - newTransaction(); - moveCursorTo (getTotalNumChars(), false); - moveCursorTo (0, true); - } - else if (key == KeyPress::returnKey) - { - newTransaction(); - - if (returnKeyStartsNewLine) - insertTextAtCursor (T("\n")); - else - returnPressed(); - } - else if (key.isKeyCode (KeyPress::escapeKey)) - { - newTransaction(); - moveCursorTo (getCaretPosition(), false); - escapePressed(); - } - else if (key.getTextCharacter() >= ' ' - || (tabKeyUsed && (key.getTextCharacter() == '\t'))) - { - insertTextAtCursor (String::charToString (key.getTextCharacter())); - - lastTransactionTime = Time::getApproximateMillisecondCounter(); - } - else - { - return false; - } - - return true; -} - -bool TextEditor::keyStateChanged() -{ - // (overridden to avoid forwarding key events to the parent) - return true; -} - -//============================================================================== -const int baseMenuItemID = 0x7fff0000; - -void TextEditor::addPopupMenuItems (PopupMenu& m, const MouseEvent*) -{ - const bool writable = ! isReadOnly(); - - m.addItem (baseMenuItemID + 1, TRANS("cut"), writable); - m.addItem (baseMenuItemID + 2, TRANS("copy"), selectionStart < selectionEnd); - m.addItem (baseMenuItemID + 3, TRANS("paste"), writable); - m.addItem (baseMenuItemID + 4, TRANS("delete"), writable); - m.addSeparator(); - m.addItem (baseMenuItemID + 5, TRANS("select all")); - m.addSeparator(); - m.addItem (baseMenuItemID + 6, TRANS("undo"), undoManager.canUndo()); - m.addItem (baseMenuItemID + 7, TRANS("redo"), undoManager.canRedo()); -} - -void TextEditor::performPopupMenuAction (const int menuItemID) -{ - switch (menuItemID) - { - case baseMenuItemID + 1: - copy(); - cut(); - break; - - case baseMenuItemID + 2: - copy(); - break; - - case baseMenuItemID + 3: - paste(); - break; - - case baseMenuItemID + 4: - cut(); - break; - - case baseMenuItemID + 5: - moveCursorTo (getTotalNumChars(), false); - moveCursorTo (0, true); - break; - - case baseMenuItemID + 6: - doUndoRedo (false); - break; - - case baseMenuItemID + 7: - doUndoRedo (true); - break; - - default: - break; - } -} - -//============================================================================== -void TextEditor::focusGained (FocusChangeType) -{ - newTransaction(); - - caretFlashState = true; - - if (selectAllTextWhenFocused) - { - moveCursorTo (0, false); - moveCursorTo (getTotalNumChars(), true); - } - - repaint(); - - if (caretVisible) - textHolder->startTimer (flashSpeedIntervalMs); - - ComponentPeer* const peer = getPeer(); - if (peer != 0) - peer->textInputRequired (getScreenX() - peer->getScreenX(), - getScreenY() - peer->getScreenY()); -} - -void TextEditor::focusLost (FocusChangeType) -{ - newTransaction(); - - wasFocused = false; - textHolder->stopTimer(); - caretFlashState = false; - - postCommandMessage (focusLossMessageId); - repaint(); -} - -//============================================================================== -void TextEditor::resized() -{ - viewport->setBoundsInset (borderSize); - viewport->setSingleStepSizes (16, roundFloatToInt (currentFont.getHeight())); - - updateTextHolderSize(); - - if (! isMultiLine()) - { - scrollToMakeSureCursorIsVisible(); - } - else - { - cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value) - - getCharPosition (caretPosition, - cursorX, cursorY, - cursorHeight); - } -} - -void TextEditor::handleCommandMessage (const int commandId) -{ - const ComponentDeletionWatcher deletionChecker (this); - - for (int i = listeners.size(); --i >= 0;) - { - TextEditorListener* const tl = (TextEditorListener*) listeners [i]; - - if (tl != 0) - { - switch (commandId) - { - case textChangeMessageId: - tl->textEditorTextChanged (*this); - break; - - case returnKeyMessageId: - tl->textEditorReturnKeyPressed (*this); - break; - - case escapeKeyMessageId: - tl->textEditorEscapeKeyPressed (*this); - break; - - case focusLossMessageId: - tl->textEditorFocusLost (*this); - break; - - default: - jassertfalse - break; - } - - if (i > 0 && deletionChecker.hasBeenDeleted()) - return; - } - } -} - -void TextEditor::enablementChanged() -{ - setMouseCursor (MouseCursor (isReadOnly() ? MouseCursor::NormalCursor - : MouseCursor::IBeamCursor)); - repaint(); -} - -//============================================================================== -void TextEditor::clearInternal (UndoManager* const um) throw() -{ - remove (0, getTotalNumChars(), um, caretPosition); -} - -void TextEditor::insert (const String& text, - const int insertIndex, - const Font& font, - const Colour& colour, - UndoManager* const um, - const int caretPositionToMoveTo) throw() -{ - if (text.isNotEmpty()) - { - if (um != 0) - { - um->perform (new TextEditorInsertAction (*this, - text, - insertIndex, - font, - colour, - caretPosition, - caretPositionToMoveTo)); - } - else - { - repaintText (insertIndex, -1); // must do this before and after changing the data, in case - // a line gets moved due to word wrap - - int index = 0; - int nextIndex = 0; - - for (int i = 0; i < sections.size(); ++i) - { - nextIndex = index + ((UniformTextSection*) sections.getUnchecked(i))->getTotalLength(); - - if (insertIndex == index) - { - sections.insert (i, new UniformTextSection (text, - font, colour, - passwordCharacter)); - break; - } - else if (insertIndex > index && insertIndex < nextIndex) - { - splitSection (i, insertIndex - index); - sections.insert (i + 1, new UniformTextSection (text, - font, colour, - passwordCharacter)); - break; - } - - index = nextIndex; - } - - if (nextIndex == insertIndex) - sections.add (new UniformTextSection (text, - font, colour, - passwordCharacter)); - - coalesceSimilarSections(); - totalNumChars = -1; - - moveCursorTo (caretPositionToMoveTo, false); - - repaintText (insertIndex, -1); - } - } -} - -void TextEditor::reinsert (const int insertIndex, - const VoidArray& sectionsToInsert) throw() -{ - int index = 0; - int nextIndex = 0; - - for (int i = 0; i < sections.size(); ++i) - { - nextIndex = index + ((UniformTextSection*) sections.getUnchecked(i))->getTotalLength(); - - if (insertIndex == index) - { - for (int j = sectionsToInsert.size(); --j >= 0;) - sections.insert (i, new UniformTextSection (*(UniformTextSection*) sectionsToInsert.getUnchecked(j))); - - break; - } - else if (insertIndex > index && insertIndex < nextIndex) - { - splitSection (i, insertIndex - index); - - for (int j = sectionsToInsert.size(); --j >= 0;) - sections.insert (i + 1, new UniformTextSection (*(UniformTextSection*) sectionsToInsert.getUnchecked(j))); - - break; - } - - index = nextIndex; - } - - if (nextIndex == insertIndex) - { - for (int j = 0; j < sectionsToInsert.size(); ++j) - sections.add (new UniformTextSection (*(UniformTextSection*) sectionsToInsert.getUnchecked(j))); - } - - coalesceSimilarSections(); - totalNumChars = -1; -} - -void TextEditor::remove (const int startIndex, - int endIndex, - UndoManager* const um, - const int caretPositionToMoveTo) throw() -{ - if (endIndex > startIndex) - { - int index = 0; - - for (int i = 0; i < sections.size(); ++i) - { - const int nextIndex = index + ((UniformTextSection*) sections[i])->getTotalLength(); - - if (startIndex > index && startIndex < nextIndex) - { - splitSection (i, startIndex - index); - --i; - } - else if (endIndex > index && endIndex < nextIndex) - { - splitSection (i, endIndex - index); - --i; - } - else - { - index = nextIndex; - - if (index > endIndex) - break; - } - } - - index = 0; - - if (um != 0) - { - VoidArray removedSections; - - for (int i = 0; i < sections.size(); ++i) - { - if (endIndex <= startIndex) - break; - - UniformTextSection* const section = (UniformTextSection*) sections.getUnchecked (i); - - const int nextIndex = index + section->getTotalLength(); - - if (startIndex <= index && endIndex >= nextIndex) - removedSections.add (new UniformTextSection (*section)); - - index = nextIndex; - } - - um->perform (new TextEditorRemoveAction (*this, - startIndex, - endIndex, - caretPosition, - caretPositionToMoveTo, - removedSections)); - } - else - { - for (int i = 0; i < sections.size(); ++i) - { - if (endIndex <= startIndex) - break; - - UniformTextSection* const section = (UniformTextSection*) sections.getUnchecked (i); - - const int nextIndex = index + section->getTotalLength(); - - if (startIndex <= index && endIndex >= nextIndex) - { - sections.remove(i); - endIndex -= (nextIndex - index); - section->clear(); - delete section; - --i; - } - else - { - index = nextIndex; - } - } - - coalesceSimilarSections(); - totalNumChars = -1; - - moveCursorTo (caretPositionToMoveTo, false); - - repaintText (startIndex, -1); - } - } -} - -//============================================================================== -const String TextEditor::getText() const throw() -{ - String t; - - for (int i = 0; i < sections.size(); ++i) - t += ((const UniformTextSection*) sections.getUnchecked(i))->getAllText(); - - return t; -} - -const String TextEditor::getTextSubstring (const int startCharacter, const int endCharacter) const throw() -{ - String t; - int index = 0; - - for (int i = 0; i < sections.size(); ++i) - { - const UniformTextSection* const s = (const UniformTextSection*) sections.getUnchecked(i); - const int nextIndex = index + s->getTotalLength(); - - if (startCharacter < nextIndex) - { - if (endCharacter <= index) - break; - - const int start = jmax (index, startCharacter); - t += s->getTextSubstring (start - index, endCharacter - index); - } - - index = nextIndex; - } - - return t; -} - -const String TextEditor::getHighlightedText() const throw() -{ - return getTextSubstring (getHighlightedRegionStart(), - getHighlightedRegionStart() + getHighlightedRegionLength()); -} - -int TextEditor::getTotalNumChars() throw() -{ - if (totalNumChars < 0) - { - totalNumChars = 0; - - for (int i = sections.size(); --i >= 0;) - totalNumChars += ((const UniformTextSection*) sections.getUnchecked(i))->getTotalLength(); - } - - return totalNumChars; -} - -bool TextEditor::isEmpty() const throw() -{ - if (totalNumChars != 0) - { - for (int i = sections.size(); --i >= 0;) - if (((const UniformTextSection*) sections.getUnchecked(i))->getTotalLength() > 0) - return false; - } - - return true; -} - -void TextEditor::getCharPosition (const int index, float& cx, float& cy, float& lineHeight) const throw() -{ - const float wordWrapWidth = getWordWrapWidth(); - - if (wordWrapWidth > 0 && sections.size() > 0) - { - TextEditorIterator i (sections, wordWrapWidth, passwordCharacter); - - i.getCharPosition (index, cx, cy, lineHeight); - } - else - { - cx = cy = 0; - lineHeight = currentFont.getHeight(); - } -} - -int TextEditor::indexAtPosition (const float x, const float y) throw() -{ - const float wordWrapWidth = getWordWrapWidth(); - - if (wordWrapWidth > 0) - { - TextEditorIterator i (sections, wordWrapWidth, passwordCharacter); - - while (i.next()) - { - if (i.lineY + getHeight() > y) - i.updateLineHeight(); - - if (i.lineY + i.lineHeight > y) - { - if (i.lineY > y) - return jmax (0, i.indexInText - 1); - - if (i.atomX >= x) - return i.indexInText; - - if (x < i.atomRight) - return i.xToIndex (x); - } - } - } - - return getTotalNumChars(); -} - -//============================================================================== -static int getCharacterCategory (const tchar character) throw() -{ - return CharacterFunctions::isLetterOrDigit (character) - ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1); -} - -int TextEditor::findWordBreakAfter (const int position) const throw() -{ - const String t (getTextSubstring (position, position + 512)); - const int totalLength = t.length(); - int i = 0; - - while (i < totalLength && CharacterFunctions::isWhitespace (t[i])) - ++i; - - const int type = getCharacterCategory (t[i]); - - while (i < totalLength && type == getCharacterCategory (t[i])) - ++i; - - while (i < totalLength && CharacterFunctions::isWhitespace (t[i])) - ++i; - - return position + i; -} - -int TextEditor::findWordBreakBefore (const int position) const throw() -{ - if (position <= 0) - return 0; - - const int startOfBuffer = jmax (0, position - 512); - const String t (getTextSubstring (startOfBuffer, position)); - - int i = position - startOfBuffer; - - while (i > 0 && CharacterFunctions::isWhitespace (t [i - 1])) - --i; - - if (i > 0) - { - const int type = getCharacterCategory (t [i - 1]); - - while (i > 0 && type == getCharacterCategory (t [i - 1])) - --i; - } - - jassert (startOfBuffer + i >= 0); - return startOfBuffer + i; -} - - -//============================================================================== -void TextEditor::splitSection (const int sectionIndex, - const int charToSplitAt) throw() -{ - jassert (sections[sectionIndex] != 0); - - sections.insert (sectionIndex + 1, - ((UniformTextSection*) sections.getUnchecked (sectionIndex)) - ->split (charToSplitAt, passwordCharacter)); -} - -void TextEditor::coalesceSimilarSections() throw() -{ - for (int i = 0; i < sections.size() - 1; ++i) - { - UniformTextSection* const s1 = (UniformTextSection*) (sections.getUnchecked (i)); - UniformTextSection* const s2 = (UniformTextSection*) (sections.getUnchecked (i + 1)); - - if (s1->font == s2->font - && s1->colour == s2->colour) - { - s1->append (*s2, passwordCharacter); - sections.remove (i + 1); - delete s2; - --i; - } - } -} - - -END_JUCE_NAMESPACE +/* + ============================================================================== + + This file is part of the JUCE library - "Jules' Utility Class Extensions" + Copyright 2004-7 by Raw Material Software ltd. + + ------------------------------------------------------------------------------ + + JUCE can be redistributed and/or modified 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. + + JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA + + ------------------------------------------------------------------------------ + + If you'd like to release a closed-source product which uses JUCE, commercial + licenses are also available: visit www.rawmaterialsoftware.com/juce for + more information. + + ============================================================================== +*/ + +#include "../../../../juce_core/basics/juce_StandardHeader.h" + +BEGIN_JUCE_NAMESPACE + +#include "juce_TextEditor.h" +#include "../../graphics/fonts/juce_GlyphArrangement.h" +#include "../../../application/juce_SystemClipboard.h" +#include "../../../../juce_core/basics/juce_Time.h" +#include "../../../../juce_core/text/juce_LocalisedStrings.h" +#include "../lookandfeel/juce_LookAndFeel.h" + +#define SHOULD_WRAP(x, wrapwidth) (((x) - 0.0001f) >= (wrapwidth)) + +//============================================================================== +// a word or space that can't be broken down any further +struct TextAtom +{ + //============================================================================== + String atomText; + float width; + uint16 numChars; + + //============================================================================== + bool isWhitespace() const throw() { return CharacterFunctions::isWhitespace (atomText[0]); } + bool isNewLine() const throw() { return atomText[0] == T('\r') || atomText[0] == T('\n'); } + + const String getText (const tchar passwordCharacter) const throw() + { + if (passwordCharacter == 0) + return atomText; + else + return String::repeatedString (String::charToString (passwordCharacter), + atomText.length()); + } + + const String getTrimmedText (const tchar passwordCharacter) const throw() + { + if (passwordCharacter == 0) + return atomText.substring (0, numChars); + else if (isNewLine()) + return String::empty; + else + return String::repeatedString (String::charToString (passwordCharacter), numChars); + } +}; + +//============================================================================== +// a run of text with a single font and colour +class UniformTextSection +{ +public: + //============================================================================== + UniformTextSection (const String& text, + const Font& font_, + const Colour& colour_, + const tchar passwordCharacter) throw() + : font (font_), + colour (colour_), + atoms (64) + { + initialiseAtoms (text, passwordCharacter); + } + + UniformTextSection (const UniformTextSection& other) throw() + : font (other.font), + colour (other.colour), + atoms (64) + { + for (int i = 0; i < other.atoms.size(); ++i) + atoms.add (new TextAtom (*(const TextAtom*) other.atoms.getUnchecked(i))); + } + + ~UniformTextSection() throw() + { + // (no need to delete the atoms, as they're explicitly deleted by the caller) + } + + void clear() throw() + { + for (int i = atoms.size(); --i >= 0;) + { + TextAtom* const atom = getAtom(i); + delete atom; + } + + atoms.clear(); + } + + int getNumAtoms() const throw() + { + return atoms.size(); + } + + TextAtom* getAtom (const int index) const throw() + { + return (TextAtom*) atoms.getUnchecked (index); + } + + void append (const UniformTextSection& other, const tchar passwordCharacter) throw() + { + if (other.atoms.size() > 0) + { + TextAtom* const lastAtom = (TextAtom*) atoms.getLast(); + int i = 0; + + if (lastAtom != 0) + { + if (! CharacterFunctions::isWhitespace (lastAtom->atomText.getLastCharacter())) + { + TextAtom* const first = other.getAtom(0); + + if (! CharacterFunctions::isWhitespace (first->atomText[0])) + { + lastAtom->atomText += first->atomText; + lastAtom->numChars = (uint16) (lastAtom->numChars + first->numChars); + lastAtom->width = font.getStringWidthFloat (lastAtom->getText (passwordCharacter)); + delete first; + ++i; + } + } + } + + while (i < other.atoms.size()) + { + atoms.add (other.getAtom(i)); + ++i; + } + } + } + + UniformTextSection* split (const int indexToBreakAt, + const tchar passwordCharacter) throw() + { + UniformTextSection* const section2 = new UniformTextSection (String::empty, + font, colour, + passwordCharacter); + int index = 0; + + for (int i = 0; i < atoms.size(); ++i) + { + TextAtom* const atom = getAtom(i); + + const int nextIndex = index + atom->numChars; + + if (index == indexToBreakAt) + { + int j; + for (j = i; j < atoms.size(); ++j) + section2->atoms.add (getAtom (j)); + + for (j = atoms.size(); --j >= i;) + atoms.remove (j); + + break; + } + else if (indexToBreakAt >= index && indexToBreakAt < nextIndex) + { + TextAtom* const secondAtom = new TextAtom(); + + secondAtom->atomText = atom->atomText.substring (indexToBreakAt - index); + secondAtom->width = font.getStringWidthFloat (secondAtom->getText (passwordCharacter)); + secondAtom->numChars = (uint16) secondAtom->atomText.length(); + + section2->atoms.add (secondAtom); + + atom->atomText = atom->atomText.substring (0, indexToBreakAt - index); + atom->width = font.getStringWidthFloat (atom->getText (passwordCharacter)); + atom->numChars = (uint16) (indexToBreakAt - index); + + int j; + for (j = i + 1; j < atoms.size(); ++j) + section2->atoms.add (getAtom (j)); + + for (j = atoms.size(); --j > i;) + atoms.remove (j); + + break; + } + + index = nextIndex; + } + + return section2; + } + + const String getAllText() const throw() + { + String s; + s.preallocateStorage (getTotalLength()); + + tchar* endOfString = (tchar*) &(s[0]); + + for (int i = 0; i < atoms.size(); ++i) + { + const TextAtom* const atom = getAtom(i); + + memcpy (endOfString, &(atom->atomText[0]), atom->numChars * sizeof (tchar)); + endOfString += atom->numChars; + } + + *endOfString = 0; + + jassert ((endOfString - (tchar*) &(s[0])) <= getTotalLength()); + return s; + } + + const String getTextSubstring (const int startCharacter, + const int endCharacter) const throw() + { + int index = 0; + int totalLen = 0; + int i; + + for (i = 0; i < atoms.size(); ++i) + { + const TextAtom* const atom = getAtom (i); + const int nextIndex = index + atom->numChars; + + if (startCharacter < nextIndex) + { + if (endCharacter <= index) + break; + + const int start = jmax (0, startCharacter - index); + const int end = jmin (endCharacter - index, atom->numChars); + jassert (end >= start); + + totalLen += end - start; + } + + index = nextIndex; + } + + String s; + s.preallocateStorage (totalLen + 1); + tchar* psz = (tchar*) (const tchar*) s; + + index = 0; + + for (i = 0; i < atoms.size(); ++i) + { + const TextAtom* const atom = getAtom (i); + const int nextIndex = index + atom->numChars; + + if (startCharacter < nextIndex) + { + if (endCharacter <= index) + break; + + const int start = jmax (0, startCharacter - index); + const int len = jmin (endCharacter - index, atom->numChars) - start; + + memcpy (psz, ((const tchar*) atom->atomText) + start, len * sizeof (tchar)); + psz += len; + *psz = 0; + } + + index = nextIndex; + } + + return s; + } + + int getTotalLength() const throw() + { + int c = 0; + + for (int i = atoms.size(); --i >= 0;) + c += getAtom(i)->numChars; + + return c; + } + + void setFont (const Font& newFont, + const tchar passwordCharacter) throw() + { + if (font != newFont) + { + font = newFont; + + for (int i = atoms.size(); --i >= 0;) + { + TextAtom* const atom = (TextAtom*) atoms.getUnchecked(i); + atom->width = newFont.getStringWidthFloat (atom->getText (passwordCharacter)); + } + } + } + + //============================================================================== + juce_UseDebuggingNewOperator + + Font font; + Colour colour; + +private: + VoidArray atoms; + + //============================================================================== + void initialiseAtoms (const String& textToParse, + const tchar passwordCharacter) throw() + { + int i = 0; + const int len = textToParse.length(); + const tchar* const text = (const tchar*) textToParse; + + while (i < len) + { + int start = i; + + // create a whitespace atom unless it starts with non-ws + if (CharacterFunctions::isWhitespace (text[i]) + && text[i] != T('\r') + && text[i] != T('\n')) + { + while (i < len + && CharacterFunctions::isWhitespace (text[i]) + && text[i] != T('\r') + && text[i] != T('\n')) + { + ++i; + } + } + else + { + if (text[i] == T('\r')) + { + ++i; + + if ((i < len) && (text[i] == T('\n'))) + { + ++start; + ++i; + } + } + else if (text[i] == T('\n')) + { + ++i; + } + else + { + while ((i < len) && ! CharacterFunctions::isWhitespace (text[i])) + ++i; + } + } + + TextAtom* const atom = new TextAtom(); + atom->atomText = String (text + start, i - start); + + atom->width = font.getStringWidthFloat (atom->getText (passwordCharacter)); + atom->numChars = (uint16) (i - start); + + atoms.add (atom); + } + } + + const UniformTextSection& operator= (const UniformTextSection& other); +}; + +//============================================================================== +class TextEditorIterator +{ +public: + //============================================================================== + TextEditorIterator (const VoidArray& sections_, + const float wordWrapWidth_, + const tchar passwordCharacter_) throw() + : indexInText (0), + lineY (0), + lineHeight (0), + maxDescent (0), + atomX (0), + atomRight (0), + atom (0), + currentSection (0), + sections (sections_), + sectionIndex (0), + atomIndex (0), + wordWrapWidth (wordWrapWidth_), + passwordCharacter (passwordCharacter_) + { + jassert (wordWrapWidth_ > 0); + + if (sections.size() > 0) + currentSection = (const UniformTextSection*) sections.getUnchecked (sectionIndex); + + if (currentSection != 0) + { + lineHeight = currentSection->font.getHeight(); + maxDescent = currentSection->font.getDescent(); + } + } + + TextEditorIterator (const TextEditorIterator& other) throw() + : indexInText (other.indexInText), + lineY (other.lineY), + lineHeight (other.lineHeight), + maxDescent (other.maxDescent), + atomX (other.atomX), + atomRight (other.atomRight), + atom (other.atom), + currentSection (other.currentSection), + sections (other.sections), + sectionIndex (other.sectionIndex), + atomIndex (other.atomIndex), + wordWrapWidth (other.wordWrapWidth), + passwordCharacter (other.passwordCharacter), + tempAtom (other.tempAtom) + { + } + + ~TextEditorIterator() throw() + { + } + + //============================================================================== + bool next() throw() + { + if (atom == &tempAtom) + { + const int numRemaining = tempAtom.atomText.length() - tempAtom.numChars; + + if (numRemaining > 0) + { + tempAtom.atomText = tempAtom.atomText.substring (tempAtom.numChars); + + atomX = 0; + + if (tempAtom.numChars > 0) + lineY += lineHeight; + + indexInText += tempAtom.numChars; + + GlyphArrangement g; + g.addLineOfText (currentSection->font, atom->getText (passwordCharacter), 0.0f, 0.0f); + + int split; + for (split = 0; split < g.getNumGlyphs(); ++split) + if (SHOULD_WRAP (g.getGlyph (split).getRight(), wordWrapWidth)) + break; + + if (split > 0 && split <= numRemaining) + { + tempAtom.numChars = (uint16) split; + tempAtom.width = g.getGlyph (split - 1).getRight(); + atomRight = atomX + tempAtom.width; + return true; + } + } + } + + bool forceNewLine = false; + + if (sectionIndex >= sections.size()) + { + moveToEndOfLastAtom(); + return false; + } + else if (atomIndex >= currentSection->getNumAtoms() - 1) + { + if (atomIndex >= currentSection->getNumAtoms()) + { + if (++sectionIndex >= sections.size()) + { + moveToEndOfLastAtom(); + return false; + } + + atomIndex = 0; + currentSection = (const UniformTextSection*) sections.getUnchecked (sectionIndex); + + lineHeight = jmax (lineHeight, currentSection->font.getHeight()); + maxDescent = jmax (maxDescent, currentSection->font.getDescent()); + } + else + { + const TextAtom* const lastAtom = currentSection->getAtom (atomIndex); + + if (! lastAtom->isWhitespace()) + { + // handle the case where the last atom in a section is actually part of the same + // word as the first atom of the next section... + float right = atomRight + lastAtom->width; + float lineHeight2 = lineHeight; + float maxDescent2 = maxDescent; + + for (int section = sectionIndex + 1; section < sections.size(); ++section) + { + const UniformTextSection* const s = (const UniformTextSection*) sections.getUnchecked (section); + + if (s->getNumAtoms() == 0) + break; + + const TextAtom* const nextAtom = s->getAtom (0); + + if (nextAtom->isWhitespace()) + break; + + right += nextAtom->width; + + lineHeight2 = jmax (lineHeight2, s->font.getHeight()); + maxDescent2 = jmax (maxDescent2, s->font.getDescent()); + + if (SHOULD_WRAP (right, wordWrapWidth)) + { + lineHeight = lineHeight2; + maxDescent = maxDescent2; + + forceNewLine = true; + break; + } + + if (s->getNumAtoms() > 1) + break; + } + } + } + } + + if (atom != 0) + { + atomX = atomRight; + indexInText += atom->numChars; + + if (atom->isNewLine()) + { + atomX = 0; + lineY += lineHeight; + } + } + + atom = currentSection->getAtom (atomIndex); + atomRight = atomX + atom->width; + ++atomIndex; + + if (SHOULD_WRAP (atomRight, wordWrapWidth) || forceNewLine) + { + if (atom->isWhitespace()) + { + // leave whitespace at the end of a line, but truncate it to avoid scrolling + atomRight = jmin (atomRight, wordWrapWidth); + } + else + { + return wrapCurrentAtom(); + } + } + + return true; + } + + bool wrapCurrentAtom() throw() + { + atomRight = atom->width; + + if (SHOULD_WRAP (atomRight, wordWrapWidth)) // atom too big to fit on a line, so break it up.. + { + tempAtom = *atom; + tempAtom.width = 0; + tempAtom.numChars = 0; + atom = &tempAtom; + + if (atomX > 0) + { + atomX = 0; + lineY += lineHeight; + } + + return next(); + } + + atomX = 0; + lineY += lineHeight; + return true; + } + + //============================================================================== + void draw (Graphics& g, const UniformTextSection*& lastSection) const throw() + { + if (passwordCharacter != 0 || ! atom->isWhitespace()) + { + if (lastSection != currentSection) + { + lastSection = currentSection; + g.setColour (currentSection->colour); + g.setFont (currentSection->font); + } + + jassert (atom->getTrimmedText (passwordCharacter).isNotEmpty()); + + GlyphArrangement ga; + ga.addLineOfText (currentSection->font, + atom->getTrimmedText (passwordCharacter), + atomX, + (float) roundFloatToInt (lineY + lineHeight - maxDescent)); + ga.draw (g); + } + } + + void drawSelection (Graphics& g, + const int selectionStart, + const int selectionEnd) const throw() + { + const int startX = roundFloatToInt (indexToX (selectionStart)); + const int endX = roundFloatToInt (indexToX (selectionEnd)); + + const int y = roundFloatToInt (lineY); + const int nextY = roundFloatToInt (lineY + lineHeight); + + g.fillRect (startX, y, endX - startX, nextY - y); + } + + void drawSelectedText (Graphics& g, + const int selectionStart, + const int selectionEnd, + const Colour& selectedTextColour) const throw() + { + if (passwordCharacter != 0 || ! atom->isWhitespace()) + { + GlyphArrangement ga; + ga.addLineOfText (currentSection->font, + atom->getTrimmedText (passwordCharacter), + atomX, + (float) roundFloatToInt (lineY + lineHeight - maxDescent)); + + if (selectionEnd < indexInText + atom->numChars) + { + GlyphArrangement ga2 (ga); + ga2.removeRangeOfGlyphs (0, selectionEnd - indexInText); + ga.removeRangeOfGlyphs (selectionEnd - indexInText, -1); + + g.setColour (currentSection->colour); + ga2.draw (g); + } + + if (selectionStart > indexInText) + { + GlyphArrangement ga2 (ga); + ga2.removeRangeOfGlyphs (selectionStart - indexInText, -1); + ga.removeRangeOfGlyphs (0, selectionStart - indexInText); + + g.setColour (currentSection->colour); + ga2.draw (g); + } + + g.setColour (selectedTextColour); + ga.draw (g); + } + } + + //============================================================================== + float indexToX (const int indexToFind) const throw() + { + if (indexToFind <= indexInText) + return atomX; + + if (indexToFind >= indexInText + atom->numChars) + return atomRight; + + GlyphArrangement g; + g.addLineOfText (currentSection->font, + atom->getText (passwordCharacter), + atomX, 0.0f); + + return jmin (atomRight, g.getGlyph (indexToFind - indexInText).getLeft()); + } + + int xToIndex (const float xToFind) const throw() + { + if (xToFind <= atomX || atom->isNewLine()) + return indexInText; + + if (xToFind >= atomRight) + return indexInText + atom->numChars; + + GlyphArrangement g; + g.addLineOfText (currentSection->font, + atom->getText (passwordCharacter), + atomX, 0.0f); + + int j; + for (j = 0; j < atom->numChars; ++j) + if ((g.getGlyph(j).getLeft() + g.getGlyph(j).getRight()) / 2 > xToFind) + break; + + return indexInText + j; + } + + //============================================================================== + void updateLineHeight() throw() + { + float x = atomRight; + + int tempSectionIndex = sectionIndex; + int tempAtomIndex = atomIndex; + const UniformTextSection* currentSection = (const UniformTextSection*) sections.getUnchecked (tempSectionIndex); + + while (! SHOULD_WRAP (x, wordWrapWidth)) + { + if (tempSectionIndex >= sections.size()) + break; + + bool checkSize = false; + + if (tempAtomIndex >= currentSection->getNumAtoms()) + { + if (++tempSectionIndex >= sections.size()) + break; + + tempAtomIndex = 0; + currentSection = (const UniformTextSection*) sections.getUnchecked (tempSectionIndex); + checkSize = true; + } + + const TextAtom* const atom = currentSection->getAtom (tempAtomIndex); + + if (atom == 0) + break; + + x += atom->width; + + if (SHOULD_WRAP (x, wordWrapWidth) || atom->isNewLine()) + break; + + if (checkSize) + { + lineHeight = jmax (lineHeight, currentSection->font.getHeight()); + maxDescent = jmax (maxDescent, currentSection->font.getDescent()); + } + + ++tempAtomIndex; + } + } + + bool getCharPosition (const int index, float& cx, float& cy, float& lineHeight_) throw() + { + while (next()) + { + if (indexInText + atom->numChars >= index) + { + updateLineHeight(); + + if (indexInText + atom->numChars > index) + { + cx = indexToX (index); + cy = lineY; + lineHeight_ = lineHeight; + return true; + } + } + } + + cx = atomX; + cy = lineY; + lineHeight_ = lineHeight; + return false; + } + + //============================================================================== + juce_UseDebuggingNewOperator + + int indexInText; + float lineY, lineHeight, maxDescent; + float atomX, atomRight; + const TextAtom* atom; + const UniformTextSection* currentSection; + +private: + const VoidArray& sections; + int sectionIndex, atomIndex; + const float wordWrapWidth; + const tchar passwordCharacter; + TextAtom tempAtom; + + const TextEditorIterator& operator= (const TextEditorIterator&); + + void moveToEndOfLastAtom() throw() + { + if (atom != 0) + { + atomX = atomRight; + + if (atom->isNewLine()) + { + atomX = 0.0f; + lineY += lineHeight; + } + } + } +}; + + +//============================================================================== +class TextEditorInsertAction : public UndoableAction +{ + TextEditor& owner; + const String text; + const int insertIndex, oldCaretPos, newCaretPos; + const Font font; + const Colour colour; + + TextEditorInsertAction (const TextEditorInsertAction&); + const TextEditorInsertAction& operator= (const TextEditorInsertAction&); + +public: + TextEditorInsertAction (TextEditor& owner_, + const String& text_, + const int insertIndex_, + const Font& font_, + const Colour& colour_, + const int oldCaretPos_, + const int newCaretPos_) throw() + : owner (owner_), + text (text_), + insertIndex (insertIndex_), + oldCaretPos (oldCaretPos_), + newCaretPos (newCaretPos_), + font (font_), + colour (colour_) + { + } + + ~TextEditorInsertAction() + { + } + + bool perform() + { + owner.insert (text, insertIndex, font, colour, 0, newCaretPos); + return true; + } + + bool undo() + { + owner.remove (insertIndex, insertIndex + text.length(), 0, oldCaretPos); + return true; + } + + int getSizeInUnits() + { + return text.length() + 16; + } +}; + +//============================================================================== +class TextEditorRemoveAction : public UndoableAction +{ + TextEditor& owner; + const int startIndex, endIndex, oldCaretPos, newCaretPos; + VoidArray removedSections; + + TextEditorRemoveAction (const TextEditorRemoveAction&); + const TextEditorRemoveAction& operator= (const TextEditorRemoveAction&); + +public: + TextEditorRemoveAction (TextEditor& owner_, + const int startIndex_, + const int endIndex_, + const int oldCaretPos_, + const int newCaretPos_, + const VoidArray& removedSections_) throw() + : owner (owner_), + startIndex (startIndex_), + endIndex (endIndex_), + oldCaretPos (oldCaretPos_), + newCaretPos (newCaretPos_), + removedSections (removedSections_) + { + } + + ~TextEditorRemoveAction() + { + for (int i = removedSections.size(); --i >= 0;) + { + UniformTextSection* const section = (UniformTextSection*) removedSections.getUnchecked (i); + section->clear(); + delete section; + } + } + + bool perform() + { + owner.remove (startIndex, endIndex, 0, newCaretPos); + return true; + } + + bool undo() + { + owner.reinsert (startIndex, removedSections); + owner.moveCursorTo (oldCaretPos, false); + return true; + } + + int getSizeInUnits() + { + int n = 0; + + for (int i = removedSections.size(); --i >= 0;) + { + UniformTextSection* const section = (UniformTextSection*) removedSections.getUnchecked (i); + n += section->getTotalLength(); + } + + return n + 16; + } +}; + +//============================================================================== +class TextHolderComponent : public Component, + public Timer +{ + TextEditor* const owner; + + TextHolderComponent (const TextHolderComponent&); + const TextHolderComponent& operator= (const TextHolderComponent&); + +public: + TextHolderComponent (TextEditor* const owner_) + : owner (owner_) + { + setWantsKeyboardFocus (false); + setInterceptsMouseClicks (false, true); + } + + ~TextHolderComponent() + { + } + + void paint (Graphics& g) + { + owner->drawContent (g); + } + + void timerCallback() + { + owner->timerCallbackInt(); + } + + const MouseCursor getMouseCursor() + { + return owner->getMouseCursor(); + } +}; + +//============================================================================== +class TextEditorViewport : public Viewport +{ + TextEditor* const owner; + float lastWordWrapWidth; + + TextEditorViewport (const TextEditorViewport&); + const TextEditorViewport& operator= (const TextEditorViewport&); + +public: + TextEditorViewport (TextEditor* const owner_) + : owner (owner_), + lastWordWrapWidth (0) + { + } + + ~TextEditorViewport() + { + } + + void visibleAreaChanged (int, int, int, int) + { + const float wordWrapWidth = owner->getWordWrapWidth(); + + if (wordWrapWidth != lastWordWrapWidth) + { + lastWordWrapWidth = wordWrapWidth; + owner->updateTextHolderSize(); + } + } +}; + +//============================================================================== +const int flashSpeedIntervalMs = 380; + +const int textChangeMessageId = 0x10003001; +const int returnKeyMessageId = 0x10003002; +const int escapeKeyMessageId = 0x10003003; +const int focusLossMessageId = 0x10003004; + + +//============================================================================== +TextEditor::TextEditor (const String& name, + const tchar passwordCharacter_) + : Component (name), + borderSize (1, 1, 1, 3), + readOnly (false), + multiline (false), + wordWrap (false), + returnKeyStartsNewLine (false), + caretVisible (true), + popupMenuEnabled (true), + selectAllTextWhenFocused (false), + scrollbarVisible (true), + wasFocused (false), + caretFlashState (true), + keepCursorOnScreen (true), + tabKeyUsed (false), + menuActive (false), + cursorX (0), + cursorY (0), + cursorHeight (0), + maxTextLength (0), + selectionStart (0), + selectionEnd (0), + leftIndent (4), + topIndent (4), + lastTransactionTime (0), + currentFont (14.0f), + totalNumChars (0), + caretPosition (0), + sections (8), + passwordCharacter (passwordCharacter_), + dragType (notDragging), + listeners (2) +{ + setOpaque (true); + + addAndMakeVisible (viewport = new TextEditorViewport (this)); + viewport->setViewedComponent (textHolder = new TextHolderComponent (this)); + viewport->setWantsKeyboardFocus (false); + viewport->setScrollBarsShown (false, false); + + setMouseCursor (MouseCursor::IBeamCursor); + setWantsKeyboardFocus (true); +} + +TextEditor::~TextEditor() +{ + clearInternal (0); + delete viewport; +} + +//============================================================================== +void TextEditor::newTransaction() throw() +{ + lastTransactionTime = Time::getApproximateMillisecondCounter(); + undoManager.beginNewTransaction(); +} + +void TextEditor::doUndoRedo (const bool isRedo) +{ + if (! isReadOnly()) + { + if ((isRedo) ? undoManager.redo() + : undoManager.undo()) + { + scrollToMakeSureCursorIsVisible(); + repaint(); + textChanged(); + } + } +} + +//============================================================================== +void TextEditor::setMultiLine (const bool shouldBeMultiLine, + const bool shouldWordWrap) +{ + multiline = shouldBeMultiLine; + wordWrap = shouldWordWrap && shouldBeMultiLine; + + setScrollbarsShown (scrollbarVisible); + + viewport->setViewPosition (0, 0); + + resized(); + scrollToMakeSureCursorIsVisible(); +} + +bool TextEditor::isMultiLine() const throw() +{ + return multiline; +} + +void TextEditor::setScrollbarsShown (bool enabled) throw() +{ + scrollbarVisible = enabled; + + enabled = enabled && isMultiLine(); + + viewport->setScrollBarsShown (enabled, enabled); +} + +void TextEditor::setReadOnly (const bool shouldBeReadOnly) +{ + readOnly = shouldBeReadOnly; + enablementChanged(); +} + +bool TextEditor::isReadOnly() const throw() +{ + return readOnly || ! isEnabled(); +} + +void TextEditor::setReturnKeyStartsNewLine (const bool shouldStartNewLine) +{ + returnKeyStartsNewLine = shouldStartNewLine; +} + +void TextEditor::setTabKeyUsedAsCharacter (const bool shouldTabKeyBeUsed) throw() +{ + tabKeyUsed = shouldTabKeyBeUsed; +} + +void TextEditor::setPopupMenuEnabled (const bool b) throw() +{ + popupMenuEnabled = b; +} + +void TextEditor::setSelectAllWhenFocused (const bool b) throw() +{ + selectAllTextWhenFocused = b; +} + +//============================================================================== +const Font TextEditor::getFont() const throw() +{ + return currentFont; +} + +void TextEditor::setFont (const Font& newFont) throw() +{ + currentFont = newFont; + scrollToMakeSureCursorIsVisible(); +} + +void TextEditor::applyFontToAllText (const Font& newFont) +{ + currentFont = newFont; + + const Colour overallColour (findColour (textColourId)); + + for (int i = sections.size(); --i >= 0;) + { + UniformTextSection* const uts = (UniformTextSection*) sections.getUnchecked(i); + uts->setFont (newFont, passwordCharacter); + uts->colour = overallColour; + } + + coalesceSimilarSections(); + updateTextHolderSize(); + scrollToMakeSureCursorIsVisible(); + repaint(); +} + +void TextEditor::colourChanged() +{ + setOpaque (findColour (backgroundColourId).isOpaque()); + repaint(); +} + +void TextEditor::setCaretVisible (const bool shouldCaretBeVisible) throw() +{ + caretVisible = shouldCaretBeVisible; + + if (shouldCaretBeVisible) + textHolder->startTimer (flashSpeedIntervalMs); + + setMouseCursor (shouldCaretBeVisible ? MouseCursor::IBeamCursor + : MouseCursor::NormalCursor); +} + +void TextEditor::setInputRestrictions (const int maxLen, + const String& chars) throw() +{ + maxTextLength = jmax (0, maxLen); + allowedCharacters = chars; +} + +void TextEditor::setTextToShowWhenEmpty (const String& text, const Colour& colourToUse) throw() +{ + textToShowWhenEmpty = text; + colourForTextWhenEmpty = colourToUse; +} + +void TextEditor::setPasswordCharacter (const tchar newPasswordCharacter) throw() +{ + if (passwordCharacter != newPasswordCharacter) + { + passwordCharacter = newPasswordCharacter; + resized(); + repaint(); + } +} + +void TextEditor::setScrollBarThickness (const int newThicknessPixels) +{ + viewport->setScrollBarThickness (newThicknessPixels); +} + +void TextEditor::setScrollBarButtonVisibility (const bool buttonsVisible) +{ + viewport->setScrollBarButtonVisibility (buttonsVisible); +} + +//============================================================================== +void TextEditor::clear() +{ + clearInternal (0); + updateTextHolderSize(); + undoManager.clearUndoHistory(); +} + +void TextEditor::setText (const String& newText, + const bool sendTextChangeMessage) +{ + const int newLength = newText.length(); + + if (newLength != getTotalNumChars() || getText() != newText) + { + const int oldCursorPos = caretPosition; + const bool cursorWasAtEnd = oldCursorPos >= getTotalNumChars(); + + clearInternal (0); + insert (newText, 0, currentFont, findColour (textColourId), 0, caretPosition); + + // if you're adding text with line-feeds to a single-line text editor, it + // ain't gonna look right! + jassert (multiline || ! newText.containsAnyOf (T("\r\n"))); + + if (cursorWasAtEnd && ! isMultiLine()) + moveCursorTo (getTotalNumChars(), false); + else + moveCursorTo (oldCursorPos, false); + + if (sendTextChangeMessage) + textChanged(); + + repaint(); + } + + updateTextHolderSize(); + scrollToMakeSureCursorIsVisible(); + undoManager.clearUndoHistory(); +} + +//============================================================================== +void TextEditor::textChanged() throw() +{ + updateTextHolderSize(); + postCommandMessage (textChangeMessageId); +} + +void TextEditor::returnPressed() +{ + postCommandMessage (returnKeyMessageId); +} + +void TextEditor::escapePressed() +{ + postCommandMessage (escapeKeyMessageId); +} + +void TextEditor::addListener (TextEditorListener* const newListener) throw() +{ + jassert (newListener != 0) + + if (newListener != 0) + listeners.add (newListener); +} + +void TextEditor::removeListener (TextEditorListener* const listenerToRemove) throw() +{ + listeners.removeValue (listenerToRemove); +} + +//============================================================================== +void TextEditor::timerCallbackInt() +{ + const bool newState = (! caretFlashState) && ! isCurrentlyBlockedByAnotherModalComponent(); + + if (caretFlashState != newState) + { + caretFlashState = newState; + + if (caretFlashState) + wasFocused = true; + + if (caretVisible + && hasKeyboardFocus (false) + && ! isReadOnly()) + { + repaintCaret(); + } + } + + const unsigned int now = Time::getApproximateMillisecondCounter(); + + if (now > lastTransactionTime + 200) + newTransaction(); +} + +void TextEditor::repaintCaret() +{ + if (! findColour (caretColourId).isTransparent()) + repaint (borderSize.getLeft() + textHolder->getX() + leftIndent + roundFloatToInt (cursorX) - 1, + borderSize.getTop() + textHolder->getY() + topIndent + roundFloatToInt (cursorY) - 1, + 4, + roundFloatToInt (cursorHeight) + 2); +} + +void TextEditor::repaintText (int textStartIndex, int textEndIndex) +{ + if (textStartIndex > textEndIndex && textEndIndex > 0) + swapVariables (textStartIndex, textEndIndex); + + float x = 0, y = 0, lh = currentFont.getHeight(); + + const float wordWrapWidth = getWordWrapWidth(); + + if (wordWrapWidth > 0) + { + TextEditorIterator i (sections, wordWrapWidth, passwordCharacter); + + i.getCharPosition (textStartIndex, x, y, lh); + + const int y1 = (int) y; + int y2; + + if (textEndIndex >= 0) + { + i.getCharPosition (textEndIndex, x, y, lh); + y2 = (int) (y + lh * 2.0f); + } + else + { + y2 = textHolder->getHeight(); + } + + textHolder->repaint (0, y1, textHolder->getWidth(), y2 - y1); + } +} + +//============================================================================== +void TextEditor::moveCaret (int newCaretPos) throw() +{ + if (newCaretPos < 0) + newCaretPos = 0; + else if (newCaretPos > getTotalNumChars()) + newCaretPos = getTotalNumChars(); + + if (newCaretPos != getCaretPosition()) + { + repaintCaret(); + caretFlashState = true; + caretPosition = newCaretPos; + textHolder->startTimer (flashSpeedIntervalMs); + scrollToMakeSureCursorIsVisible(); + repaintCaret(); + } +} + +void TextEditor::setCaretPosition (const int newIndex) throw() +{ + moveCursorTo (newIndex, false); +} + +int TextEditor::getCaretPosition() const throw() +{ + return caretPosition; +} + +void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX, + const int desiredCaretY) throw() + +{ + updateCaretPosition(); + + int vx = roundFloatToInt (cursorX) - desiredCaretX; + int vy = roundFloatToInt (cursorY) - desiredCaretY; + + if (desiredCaretX < jmax (1, proportionOfWidth (0.05f))) + { + vx += desiredCaretX - proportionOfWidth (0.2f); + } + else if (desiredCaretX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10))) + { + vx += desiredCaretX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth(); + } + + vx = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), vx); + + if (! isMultiLine()) + { + vy = viewport->getViewPositionY(); + } + else + { + vy = jlimit (0, jmax (0, textHolder->getHeight() - viewport->getMaximumVisibleHeight()), vy); + + const int curH = roundFloatToInt (cursorHeight); + + if (desiredCaretY < 0) + { + vy = jmax (0, desiredCaretY + vy); + } + else if (desiredCaretY > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - curH)) + { + vy += desiredCaretY + 2 + curH + topIndent - viewport->getMaximumVisibleHeight(); + } + } + + viewport->setViewPosition (vx, vy); +} + +const Rectangle TextEditor::getCaretRectangle() throw() +{ + updateCaretPosition(); + + return Rectangle (roundFloatToInt (cursorX) - viewport->getX(), + roundFloatToInt (cursorY) - viewport->getY(), + 1, roundFloatToInt (cursorHeight)); +} + +//============================================================================== +float TextEditor::getWordWrapWidth() const throw() +{ + return (wordWrap) ? (float) (viewport->getMaximumVisibleWidth() - leftIndent - leftIndent / 2) + : 1.0e10f; +} + +void TextEditor::updateTextHolderSize() throw() +{ + const float wordWrapWidth = getWordWrapWidth(); + + if (wordWrapWidth > 0) + { + float maxWidth = 0.0f; + + TextEditorIterator i (sections, wordWrapWidth, passwordCharacter); + + while (i.next()) + maxWidth = jmax (maxWidth, i.atomRight); + + const int w = leftIndent + roundFloatToInt (maxWidth); + const int h = topIndent + roundFloatToInt (jmax (i.lineY + i.lineHeight, + currentFont.getHeight())); + + textHolder->setSize (w + 1, h + 1); + } +} + +int TextEditor::getTextWidth() const throw() +{ + return textHolder->getWidth(); +} + +int TextEditor::getTextHeight() const throw() +{ + return textHolder->getHeight(); +} + +void TextEditor::setIndents (const int newLeftIndent, + const int newTopIndent) throw() +{ + leftIndent = newLeftIndent; + topIndent = newTopIndent; +} + +void TextEditor::setBorder (const BorderSize& border) throw() +{ + borderSize = border; + resized(); +} + +const BorderSize TextEditor::getBorder() const throw() +{ + return borderSize; +} + +void TextEditor::setScrollToShowCursor (const bool shouldScrollToShowCursor) throw() +{ + keepCursorOnScreen = shouldScrollToShowCursor; +} + +void TextEditor::updateCaretPosition() throw() +{ + cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value) + getCharPosition (caretPosition, cursorX, cursorY, cursorHeight); +} + +void TextEditor::scrollToMakeSureCursorIsVisible() throw() +{ + updateCaretPosition(); + + if (keepCursorOnScreen) + { + int x = viewport->getViewPositionX(); + int y = viewport->getViewPositionY(); + + const int relativeCursorX = roundFloatToInt (cursorX) - x; + const int relativeCursorY = roundFloatToInt (cursorY) - y; + + if (relativeCursorX < jmax (1, proportionOfWidth (0.05f))) + { + x += relativeCursorX - proportionOfWidth (0.2f); + } + else if (relativeCursorX > jmax (0, viewport->getMaximumVisibleWidth() - (wordWrap ? 2 : 10))) + { + x += relativeCursorX + (isMultiLine() ? proportionOfWidth (0.2f) : 10) - viewport->getMaximumVisibleWidth(); + } + + x = jlimit (0, jmax (0, textHolder->getWidth() + 8 - viewport->getMaximumVisibleWidth()), x); + + if (! isMultiLine()) + { + y = (getHeight() - textHolder->getHeight() - topIndent) / -2; + } + else + { + const int curH = roundFloatToInt (cursorHeight); + + if (relativeCursorY < 0) + { + y = jmax (0, relativeCursorY + y); + } + else if (relativeCursorY > jmax (0, viewport->getMaximumVisibleHeight() - topIndent - curH)) + { + y += relativeCursorY + 2 + curH + topIndent - viewport->getMaximumVisibleHeight(); + } + } + + viewport->setViewPosition (x, y); + } +} + +void TextEditor::moveCursorTo (const int newPosition, + const bool isSelecting) throw() +{ + if (isSelecting) + { + moveCaret (newPosition); + + const int oldSelStart = selectionStart; + const int oldSelEnd = selectionEnd; + + if (dragType == notDragging) + { + if (abs (getCaretPosition() - selectionStart) < abs (getCaretPosition() - selectionEnd)) + dragType = draggingSelectionStart; + else + dragType = draggingSelectionEnd; + } + + if (dragType == draggingSelectionStart) + { + selectionStart = getCaretPosition(); + + if (selectionEnd < selectionStart) + { + swapVariables (selectionStart, selectionEnd); + dragType = draggingSelectionEnd; + } + } + else + { + selectionEnd = getCaretPosition(); + + if (selectionEnd < selectionStart) + { + swapVariables (selectionStart, selectionEnd); + dragType = draggingSelectionStart; + } + } + + jassert (selectionStart <= selectionEnd); + jassert (oldSelStart <= oldSelEnd); + + repaintText (jmin (oldSelStart, selectionStart), + jmax (oldSelEnd, selectionEnd)); + } + else + { + dragType = notDragging; + + if (selectionEnd > selectionStart) + repaintText (selectionStart, selectionEnd); + + moveCaret (newPosition); + selectionStart = getCaretPosition(); + selectionEnd = getCaretPosition(); + } +} + +int TextEditor::getTextIndexAt (const int x, + const int y) throw() +{ + return indexAtPosition ((float) (x + viewport->getViewPositionX() - leftIndent), + (float) (y + viewport->getViewPositionY() - topIndent)); +} + +void TextEditor::insertTextAtCursor (String newText) +{ + if (allowedCharacters.isNotEmpty()) + newText = newText.retainCharacters (allowedCharacters); + + if (! isMultiLine()) + newText = newText.replaceCharacters (T("\r\n"), T(" ")); + else + newText = newText.replace (T("\r\n"), T("\n")); + + const int newCaretPos = selectionStart + newText.length(); + const int insertIndex = selectionStart; + + remove (selectionStart, selectionEnd, + &undoManager, + newCaretPos - 1); + + if (maxTextLength > 0) + newText = newText.substring (0, maxTextLength - getTotalNumChars()); + + if (newText.isNotEmpty()) + insert (newText, + insertIndex, + currentFont, + findColour (textColourId), + &undoManager, + newCaretPos); + + textChanged(); +} + +void TextEditor::setHighlightedRegion (int startPos, int numChars) throw() +{ + moveCursorTo (startPos, false); + moveCursorTo (startPos + numChars, true); +} + +//============================================================================== +void TextEditor::copy() +{ + const String selection (getTextSubstring (selectionStart, selectionEnd)); + + if (selection.isNotEmpty()) + SystemClipboard::copyTextToClipboard (selection); +} + +void TextEditor::paste() +{ + if (! isReadOnly()) + { + const String clip (SystemClipboard::getTextFromClipboard()); + + if (clip.isNotEmpty()) + insertTextAtCursor (clip); + } +} + +void TextEditor::cut() +{ + if (! isReadOnly()) + { + moveCaret (selectionEnd); + insertTextAtCursor (String::empty); + } +} + +//============================================================================== +void TextEditor::drawContent (Graphics& g) +{ + const float wordWrapWidth = getWordWrapWidth(); + + if (wordWrapWidth > 0) + { + g.setOrigin (leftIndent, topIndent); + const Rectangle clip (g.getClipBounds()); + Colour selectedTextColour; + + TextEditorIterator i (sections, wordWrapWidth, passwordCharacter); + + while (i.lineY + 200.0 < clip.getY() && i.next()) + {} + + if (selectionStart < selectionEnd) + { + g.setColour (findColour (highlightColourId) + .withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f)); + + selectedTextColour = findColour (highlightedTextColourId); + + TextEditorIterator i2 (i); + + while (i2.next() && i2.lineY < clip.getBottom()) + { + i2.updateLineHeight(); + + if (i2.lineY + i2.lineHeight >= clip.getY() + && selectionEnd >= i2.indexInText + && selectionStart <= i2.indexInText + i2.atom->numChars) + { + i2.drawSelection (g, selectionStart, selectionEnd); + } + } + } + + const UniformTextSection* lastSection = 0; + + while (i.next() && i.lineY < clip.getBottom()) + { + i.updateLineHeight(); + + if (i.lineY + i.lineHeight >= clip.getY()) + { + if (selectionEnd >= i.indexInText + && selectionStart <= i.indexInText + i.atom->numChars) + { + i.drawSelectedText (g, selectionStart, selectionEnd, selectedTextColour); + lastSection = 0; + } + else + { + i.draw (g, lastSection); + } + } + } + } +} + +void TextEditor::paint (Graphics& g) +{ + getLookAndFeel().fillTextEditorBackground (g, getWidth(), getHeight(), *this); +} + +void TextEditor::paintOverChildren (Graphics& g) +{ + if (caretFlashState + && hasKeyboardFocus (false) + && caretVisible + && ! isReadOnly()) + { + g.setColour (findColour (caretColourId)); + + g.fillRect (borderSize.getLeft() + textHolder->getX() + leftIndent + cursorX, + borderSize.getTop() + textHolder->getY() + topIndent + cursorY, + 2.0f, cursorHeight); + } + + if (textToShowWhenEmpty.isNotEmpty() + && (! hasKeyboardFocus (false)) + && getTotalNumChars() == 0) + { + g.setColour (colourForTextWhenEmpty); + g.setFont (getFont()); + + if (isMultiLine()) + { + g.drawText (textToShowWhenEmpty, + 0, 0, getWidth(), getHeight(), + Justification::centred, true); + } + else + { + g.drawText (textToShowWhenEmpty, + leftIndent, topIndent, + viewport->getWidth() - leftIndent, + viewport->getHeight() - topIndent, + Justification::centredLeft, true); + } + } + + getLookAndFeel().drawTextEditorOutline (g, getWidth(), getHeight(), *this); +} + +//============================================================================== +void TextEditor::mouseDown (const MouseEvent& e) +{ + beginDragAutoRepeat (100); + newTransaction(); + + if (wasFocused || ! selectAllTextWhenFocused) + { + if (! (popupMenuEnabled && e.mods.isPopupMenu())) + { + moveCursorTo (getTextIndexAt (e.x, e.y), + e.mods.isShiftDown()); + } + else + { + PopupMenu m; + addPopupMenuItems (m, &e); + + menuActive = true; + const int result = m.show(); + menuActive = false; + + if (result != 0) + performPopupMenuAction (result); + } + } +} + +void TextEditor::mouseDrag (const MouseEvent& e) +{ + if (wasFocused || ! selectAllTextWhenFocused) + { + if (! (popupMenuEnabled && e.mods.isPopupMenu())) + { + moveCursorTo (getTextIndexAt (e.x, e.y), true); + } + } +} + +void TextEditor::mouseUp (const MouseEvent& e) +{ + newTransaction(); + textHolder->startTimer (flashSpeedIntervalMs); + + if (wasFocused || ! selectAllTextWhenFocused) + { + if (! (popupMenuEnabled && e.mods.isPopupMenu())) + { + moveCaret (getTextIndexAt (e.x, e.y)); + } + } + + wasFocused = true; +} + +void TextEditor::mouseDoubleClick (const MouseEvent& e) +{ + int tokenEnd = getTextIndexAt (e.x, e.y); + int tokenStart = tokenEnd; + + if (e.getNumberOfClicks() > 3) + { + tokenStart = 0; + tokenEnd = getTotalNumChars(); + } + else + { + const String t (getText()); + const int totalLength = getTotalNumChars(); + + while (tokenEnd < totalLength) + { + if (CharacterFunctions::isLetterOrDigit (t [tokenEnd])) + ++tokenEnd; + else + break; + } + + tokenStart = tokenEnd; + + while (tokenStart > 0) + { + if (CharacterFunctions::isLetterOrDigit (t [tokenStart - 1])) + --tokenStart; + else + break; + } + + if (e.getNumberOfClicks() > 2) + { + while (tokenEnd < totalLength) + { + if (t [tokenEnd] != T('\r') && t [tokenEnd] != T('\n')) + ++tokenEnd; + else + break; + } + + while (tokenStart > 0) + { + if (t [tokenStart - 1] != T('\r') && t [tokenStart - 1] != T('\n')) + --tokenStart; + else + break; + } + } + } + + moveCursorTo (tokenEnd, false); + moveCursorTo (tokenStart, true); +} + +void TextEditor::mouseWheelMove (const MouseEvent& e, float wheelIncrementX, float wheelIncrementY) +{ + if (! viewport->useMouseWheelMoveIfNeeded (e, wheelIncrementX, wheelIncrementY)) + Component::mouseWheelMove (e, wheelIncrementX, wheelIncrementY); +} + +//============================================================================== +bool TextEditor::keyPressed (const KeyPress& key) +{ + if (isReadOnly() && key != KeyPress (T('c'), ModifierKeys::commandModifier, 0)) + return false; + + const bool moveInWholeWordSteps = key.getModifiers().isCtrlDown() || key.getModifiers().isAltDown(); + + if (key.isKeyCode (KeyPress::leftKey) + || key.isKeyCode (KeyPress::upKey)) + { + newTransaction(); + + int newPos; + + if (isMultiLine() && key.isKeyCode (KeyPress::upKey)) + newPos = indexAtPosition (cursorX, cursorY - 1); + else if (moveInWholeWordSteps) + newPos = findWordBreakBefore (getCaretPosition()); + else + newPos = getCaretPosition() - 1; + + moveCursorTo (newPos, key.getModifiers().isShiftDown()); + } + else if (key.isKeyCode (KeyPress::rightKey) + || key.isKeyCode (KeyPress::downKey)) + { + newTransaction(); + + int newPos; + + if (isMultiLine() && key.isKeyCode (KeyPress::downKey)) + newPos = indexAtPosition (cursorX, cursorY + cursorHeight + 1); + else if (moveInWholeWordSteps) + newPos = findWordBreakAfter (getCaretPosition()); + else + newPos = getCaretPosition() + 1; + + moveCursorTo (newPos, key.getModifiers().isShiftDown()); + } + else if (key.isKeyCode (KeyPress::pageDownKey) && isMultiLine()) + { + newTransaction(); + + moveCursorTo (indexAtPosition (cursorX, cursorY + cursorHeight + viewport->getViewHeight()), + key.getModifiers().isShiftDown()); + } + else if (key.isKeyCode (KeyPress::pageUpKey) && isMultiLine()) + { + newTransaction(); + + moveCursorTo (indexAtPosition (cursorX, cursorY - viewport->getViewHeight()), + key.getModifiers().isShiftDown()); + } + else if (key.isKeyCode (KeyPress::homeKey)) + { + newTransaction(); + + if (isMultiLine() && ! moveInWholeWordSteps) + moveCursorTo (indexAtPosition (0.0f, cursorY), + key.getModifiers().isShiftDown()); + else + moveCursorTo (0, key.getModifiers().isShiftDown()); + } + else if (key.isKeyCode (KeyPress::endKey)) + { + newTransaction(); + + if (isMultiLine() && ! moveInWholeWordSteps) + moveCursorTo (indexAtPosition ((float) textHolder->getWidth(), cursorY), + key.getModifiers().isShiftDown()); + else + moveCursorTo (getTotalNumChars(), key.getModifiers().isShiftDown()); + } + else if (key.isKeyCode (KeyPress::backspaceKey)) + { + if (moveInWholeWordSteps) + { + moveCursorTo (findWordBreakBefore (getCaretPosition()), true); + } + else + { + if (selectionStart == selectionEnd && selectionStart > 0) + --selectionStart; + } + + cut(); + } + else if (key.isKeyCode (KeyPress::deleteKey)) + { + if (key.getModifiers().isShiftDown()) + copy(); + + if (selectionStart == selectionEnd + && selectionEnd < getTotalNumChars()) + { + ++selectionEnd; + } + + cut(); + } + else if (key == KeyPress (T('c'), ModifierKeys::commandModifier, 0) + || key == KeyPress (KeyPress::insertKey, ModifierKeys::ctrlModifier, 0)) + { + newTransaction(); + copy(); + } + else if (key == KeyPress (T('x'), ModifierKeys::commandModifier, 0)) + { + newTransaction(); + copy(); + cut(); + } + else if (key == KeyPress (T('v'), ModifierKeys::commandModifier, 0) + || key == KeyPress (KeyPress::insertKey, ModifierKeys::shiftModifier, 0)) + { + newTransaction(); + paste(); + } + else if (key == KeyPress (T('z'), ModifierKeys::commandModifier, 0)) + { + newTransaction(); + doUndoRedo (false); + } + else if (key == KeyPress (T('y'), ModifierKeys::commandModifier, 0)) + { + newTransaction(); + doUndoRedo (true); + } + else if (key == KeyPress (T('a'), ModifierKeys::commandModifier, 0)) + { + newTransaction(); + moveCursorTo (getTotalNumChars(), false); + moveCursorTo (0, true); + } + else if (key == KeyPress::returnKey) + { + newTransaction(); + + if (returnKeyStartsNewLine) + insertTextAtCursor (T("\n")); + else + returnPressed(); + } + else if (key.isKeyCode (KeyPress::escapeKey)) + { + newTransaction(); + moveCursorTo (getCaretPosition(), false); + escapePressed(); + } + else if (key.getTextCharacter() >= ' ' + || (tabKeyUsed && (key.getTextCharacter() == '\t'))) + { + insertTextAtCursor (String::charToString (key.getTextCharacter())); + + lastTransactionTime = Time::getApproximateMillisecondCounter(); + } + else + { + return false; + } + + return true; +} + +bool TextEditor::keyStateChanged() +{ + // (overridden to avoid forwarding key events to the parent) + return true; +} + +//============================================================================== +const int baseMenuItemID = 0x7fff0000; + +void TextEditor::addPopupMenuItems (PopupMenu& m, const MouseEvent*) +{ + const bool writable = ! isReadOnly(); + + m.addItem (baseMenuItemID + 1, TRANS("cut"), writable); + m.addItem (baseMenuItemID + 2, TRANS("copy"), selectionStart < selectionEnd); + m.addItem (baseMenuItemID + 3, TRANS("paste"), writable); + m.addItem (baseMenuItemID + 4, TRANS("delete"), writable); + m.addSeparator(); + m.addItem (baseMenuItemID + 5, TRANS("select all")); + m.addSeparator(); + m.addItem (baseMenuItemID + 6, TRANS("undo"), undoManager.canUndo()); + m.addItem (baseMenuItemID + 7, TRANS("redo"), undoManager.canRedo()); +} + +void TextEditor::performPopupMenuAction (const int menuItemID) +{ + switch (menuItemID) + { + case baseMenuItemID + 1: + copy(); + cut(); + break; + + case baseMenuItemID + 2: + copy(); + break; + + case baseMenuItemID + 3: + paste(); + break; + + case baseMenuItemID + 4: + cut(); + break; + + case baseMenuItemID + 5: + moveCursorTo (getTotalNumChars(), false); + moveCursorTo (0, true); + break; + + case baseMenuItemID + 6: + doUndoRedo (false); + break; + + case baseMenuItemID + 7: + doUndoRedo (true); + break; + + default: + break; + } +} + +//============================================================================== +void TextEditor::focusGained (FocusChangeType) +{ + newTransaction(); + + caretFlashState = true; + + if (selectAllTextWhenFocused) + { + moveCursorTo (0, false); + moveCursorTo (getTotalNumChars(), true); + } + + repaint(); + + if (caretVisible) + textHolder->startTimer (flashSpeedIntervalMs); + + ComponentPeer* const peer = getPeer(); + if (peer != 0) + peer->textInputRequired (getScreenX() - peer->getScreenX(), + getScreenY() - peer->getScreenY()); +} + +void TextEditor::focusLost (FocusChangeType) +{ + newTransaction(); + + wasFocused = false; + textHolder->stopTimer(); + caretFlashState = false; + + postCommandMessage (focusLossMessageId); + repaint(); +} + +//============================================================================== +void TextEditor::resized() +{ + viewport->setBoundsInset (borderSize); + viewport->setSingleStepSizes (16, roundFloatToInt (currentFont.getHeight())); + + updateTextHolderSize(); + + if (! isMultiLine()) + { + scrollToMakeSureCursorIsVisible(); + } + else + { + updateCaretPosition(); + } +} + +void TextEditor::handleCommandMessage (const int commandId) +{ + const ComponentDeletionWatcher deletionChecker (this); + + for (int i = listeners.size(); --i >= 0;) + { + TextEditorListener* const tl = (TextEditorListener*) listeners [i]; + + if (tl != 0) + { + switch (commandId) + { + case textChangeMessageId: + tl->textEditorTextChanged (*this); + break; + + case returnKeyMessageId: + tl->textEditorReturnKeyPressed (*this); + break; + + case escapeKeyMessageId: + tl->textEditorEscapeKeyPressed (*this); + break; + + case focusLossMessageId: + tl->textEditorFocusLost (*this); + break; + + default: + jassertfalse + break; + } + + if (i > 0 && deletionChecker.hasBeenDeleted()) + return; + } + } +} + +void TextEditor::enablementChanged() +{ + setMouseCursor (MouseCursor (isReadOnly() ? MouseCursor::NormalCursor + : MouseCursor::IBeamCursor)); + repaint(); +} + +//============================================================================== +void TextEditor::clearInternal (UndoManager* const um) throw() +{ + remove (0, getTotalNumChars(), um, caretPosition); +} + +void TextEditor::insert (const String& text, + const int insertIndex, + const Font& font, + const Colour& colour, + UndoManager* const um, + const int caretPositionToMoveTo) throw() +{ + if (text.isNotEmpty()) + { + if (um != 0) + { + um->perform (new TextEditorInsertAction (*this, + text, + insertIndex, + font, + colour, + caretPosition, + caretPositionToMoveTo)); + } + else + { + repaintText (insertIndex, -1); // must do this before and after changing the data, in case + // a line gets moved due to word wrap + + int index = 0; + int nextIndex = 0; + + for (int i = 0; i < sections.size(); ++i) + { + nextIndex = index + ((UniformTextSection*) sections.getUnchecked(i))->getTotalLength(); + + if (insertIndex == index) + { + sections.insert (i, new UniformTextSection (text, + font, colour, + passwordCharacter)); + break; + } + else if (insertIndex > index && insertIndex < nextIndex) + { + splitSection (i, insertIndex - index); + sections.insert (i + 1, new UniformTextSection (text, + font, colour, + passwordCharacter)); + break; + } + + index = nextIndex; + } + + if (nextIndex == insertIndex) + sections.add (new UniformTextSection (text, + font, colour, + passwordCharacter)); + + coalesceSimilarSections(); + totalNumChars = -1; + + moveCursorTo (caretPositionToMoveTo, false); + + repaintText (insertIndex, -1); + } + } +} + +void TextEditor::reinsert (const int insertIndex, + const VoidArray& sectionsToInsert) throw() +{ + int index = 0; + int nextIndex = 0; + + for (int i = 0; i < sections.size(); ++i) + { + nextIndex = index + ((UniformTextSection*) sections.getUnchecked(i))->getTotalLength(); + + if (insertIndex == index) + { + for (int j = sectionsToInsert.size(); --j >= 0;) + sections.insert (i, new UniformTextSection (*(UniformTextSection*) sectionsToInsert.getUnchecked(j))); + + break; + } + else if (insertIndex > index && insertIndex < nextIndex) + { + splitSection (i, insertIndex - index); + + for (int j = sectionsToInsert.size(); --j >= 0;) + sections.insert (i + 1, new UniformTextSection (*(UniformTextSection*) sectionsToInsert.getUnchecked(j))); + + break; + } + + index = nextIndex; + } + + if (nextIndex == insertIndex) + { + for (int j = 0; j < sectionsToInsert.size(); ++j) + sections.add (new UniformTextSection (*(UniformTextSection*) sectionsToInsert.getUnchecked(j))); + } + + coalesceSimilarSections(); + totalNumChars = -1; +} + +void TextEditor::remove (const int startIndex, + int endIndex, + UndoManager* const um, + const int caretPositionToMoveTo) throw() +{ + if (endIndex > startIndex) + { + int index = 0; + + for (int i = 0; i < sections.size(); ++i) + { + const int nextIndex = index + ((UniformTextSection*) sections[i])->getTotalLength(); + + if (startIndex > index && startIndex < nextIndex) + { + splitSection (i, startIndex - index); + --i; + } + else if (endIndex > index && endIndex < nextIndex) + { + splitSection (i, endIndex - index); + --i; + } + else + { + index = nextIndex; + + if (index > endIndex) + break; + } + } + + index = 0; + + if (um != 0) + { + VoidArray removedSections; + + for (int i = 0; i < sections.size(); ++i) + { + if (endIndex <= startIndex) + break; + + UniformTextSection* const section = (UniformTextSection*) sections.getUnchecked (i); + + const int nextIndex = index + section->getTotalLength(); + + if (startIndex <= index && endIndex >= nextIndex) + removedSections.add (new UniformTextSection (*section)); + + index = nextIndex; + } + + um->perform (new TextEditorRemoveAction (*this, + startIndex, + endIndex, + caretPosition, + caretPositionToMoveTo, + removedSections)); + } + else + { + for (int i = 0; i < sections.size(); ++i) + { + if (endIndex <= startIndex) + break; + + UniformTextSection* const section = (UniformTextSection*) sections.getUnchecked (i); + + const int nextIndex = index + section->getTotalLength(); + + if (startIndex <= index && endIndex >= nextIndex) + { + sections.remove(i); + endIndex -= (nextIndex - index); + section->clear(); + delete section; + --i; + } + else + { + index = nextIndex; + } + } + + coalesceSimilarSections(); + totalNumChars = -1; + + moveCursorTo (caretPositionToMoveTo, false); + + repaintText (startIndex, -1); + } + } +} + +//============================================================================== +const String TextEditor::getText() const throw() +{ + String t; + + for (int i = 0; i < sections.size(); ++i) + t += ((const UniformTextSection*) sections.getUnchecked(i))->getAllText(); + + return t; +} + +const String TextEditor::getTextSubstring (const int startCharacter, const int endCharacter) const throw() +{ + String t; + int index = 0; + + for (int i = 0; i < sections.size(); ++i) + { + const UniformTextSection* const s = (const UniformTextSection*) sections.getUnchecked(i); + const int nextIndex = index + s->getTotalLength(); + + if (startCharacter < nextIndex) + { + if (endCharacter <= index) + break; + + const int start = jmax (index, startCharacter); + t += s->getTextSubstring (start - index, endCharacter - index); + } + + index = nextIndex; + } + + return t; +} + +const String TextEditor::getHighlightedText() const throw() +{ + return getTextSubstring (getHighlightedRegionStart(), + getHighlightedRegionStart() + getHighlightedRegionLength()); +} + +int TextEditor::getTotalNumChars() throw() +{ + if (totalNumChars < 0) + { + totalNumChars = 0; + + for (int i = sections.size(); --i >= 0;) + totalNumChars += ((const UniformTextSection*) sections.getUnchecked(i))->getTotalLength(); + } + + return totalNumChars; +} + +bool TextEditor::isEmpty() const throw() +{ + if (totalNumChars != 0) + { + for (int i = sections.size(); --i >= 0;) + if (((const UniformTextSection*) sections.getUnchecked(i))->getTotalLength() > 0) + return false; + } + + return true; +} + +void TextEditor::getCharPosition (const int index, float& cx, float& cy, float& lineHeight) const throw() +{ + const float wordWrapWidth = getWordWrapWidth(); + + if (wordWrapWidth > 0 && sections.size() > 0) + { + TextEditorIterator i (sections, wordWrapWidth, passwordCharacter); + + i.getCharPosition (index, cx, cy, lineHeight); + } + else + { + cx = cy = 0; + lineHeight = currentFont.getHeight(); + } +} + +int TextEditor::indexAtPosition (const float x, const float y) throw() +{ + const float wordWrapWidth = getWordWrapWidth(); + + if (wordWrapWidth > 0) + { + TextEditorIterator i (sections, wordWrapWidth, passwordCharacter); + + while (i.next()) + { + if (i.lineY + getHeight() > y) + i.updateLineHeight(); + + if (i.lineY + i.lineHeight > y) + { + if (i.lineY > y) + return jmax (0, i.indexInText - 1); + + if (i.atomX >= x) + return i.indexInText; + + if (x < i.atomRight) + return i.xToIndex (x); + } + } + } + + return getTotalNumChars(); +} + +//============================================================================== +static int getCharacterCategory (const tchar character) throw() +{ + return CharacterFunctions::isLetterOrDigit (character) + ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1); +} + +int TextEditor::findWordBreakAfter (const int position) const throw() +{ + const String t (getTextSubstring (position, position + 512)); + const int totalLength = t.length(); + int i = 0; + + while (i < totalLength && CharacterFunctions::isWhitespace (t[i])) + ++i; + + const int type = getCharacterCategory (t[i]); + + while (i < totalLength && type == getCharacterCategory (t[i])) + ++i; + + while (i < totalLength && CharacterFunctions::isWhitespace (t[i])) + ++i; + + return position + i; +} + +int TextEditor::findWordBreakBefore (const int position) const throw() +{ + if (position <= 0) + return 0; + + const int startOfBuffer = jmax (0, position - 512); + const String t (getTextSubstring (startOfBuffer, position)); + + int i = position - startOfBuffer; + + while (i > 0 && CharacterFunctions::isWhitespace (t [i - 1])) + --i; + + if (i > 0) + { + const int type = getCharacterCategory (t [i - 1]); + + while (i > 0 && type == getCharacterCategory (t [i - 1])) + --i; + } + + jassert (startOfBuffer + i >= 0); + return startOfBuffer + i; +} + + +//============================================================================== +void TextEditor::splitSection (const int sectionIndex, + const int charToSplitAt) throw() +{ + jassert (sections[sectionIndex] != 0); + + sections.insert (sectionIndex + 1, + ((UniformTextSection*) sections.getUnchecked (sectionIndex)) + ->split (charToSplitAt, passwordCharacter)); +} + +void TextEditor::coalesceSimilarSections() throw() +{ + for (int i = 0; i < sections.size() - 1; ++i) + { + UniformTextSection* const s1 = (UniformTextSection*) (sections.getUnchecked (i)); + UniformTextSection* const s2 = (UniformTextSection*) (sections.getUnchecked (i + 1)); + + if (s1->font == s2->font + && s1->colour == s2->colour) + { + s1->append (*s2, passwordCharacter); + sections.remove (i + 1); + delete s2; + --i; + } + } +} + + +END_JUCE_NAMESPACE diff --git a/src/juce_appframework/gui/components/controls/juce_TextEditor.h b/src/juce_appframework/gui/components/controls/juce_TextEditor.h index 4a1c8ab652..b4a7763165 100644 --- a/src/juce_appframework/gui/components/controls/juce_TextEditor.h +++ b/src/juce_appframework/gui/components/controls/juce_TextEditor.h @@ -1,684 +1,707 @@ -/* - ============================================================================== - - This file is part of the JUCE library - "Jules' Utility Class Extensions" - Copyright 2004-7 by Raw Material Software ltd. - - ------------------------------------------------------------------------------ - - JUCE can be redistributed and/or modified 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. - - JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the - Free Software Foundation, Inc., 59 Temple Place, Suite 330, - Boston, MA 02111-1307 USA - - ------------------------------------------------------------------------------ - - If you'd like to release a closed-source product which uses JUCE, commercial - licenses are also available: visit www.rawmaterialsoftware.com/juce for - more information. - - ============================================================================== -*/ - -#ifndef __JUCE_TEXTEDITOR_JUCEHEADER__ -#define __JUCE_TEXTEDITOR_JUCEHEADER__ - -#include "../juce_Component.h" -#include "../../../events/juce_Timer.h" -#include "../../../documents/juce_UndoManager.h" -#include "../layout/juce_Viewport.h" -#include "../menus/juce_PopupMenu.h" -class TextEditor; -class TextHolderComponent; - - -//============================================================================== -/** - Receives callbacks from a TextEditor component when it changes. - - @see TextEditor::addListener -*/ -class JUCE_API TextEditorListener -{ -public: - /** Destructor. */ - virtual ~TextEditorListener() {} - - /** Called when the user changes the text in some way. */ - virtual void textEditorTextChanged (TextEditor& editor) = 0; - - /** Called when the user presses the return key. */ - virtual void textEditorReturnKeyPressed (TextEditor& editor) = 0; - - /** Called when the user presses the escape key. */ - virtual void textEditorEscapeKeyPressed (TextEditor& editor) = 0; - - /** Called when the text editor loses focus. */ - virtual void textEditorFocusLost (TextEditor& editor) = 0; -}; - - -//============================================================================== -/** - A component containing text that can be edited. - - A TextEditor can either be in single- or multi-line mode, and supports mixed - fonts and colours. - - @see TextEditorListener, Label -*/ -class JUCE_API TextEditor : public Component, - public SettableTooltipClient -{ -public: - //============================================================================== - /** Creates a new, empty text editor. - - @param componentName the name to pass to the component for it to use as its name - @param passwordCharacter if this is not zero, this character will be used as a replacement - for all characters that are drawn on screen - e.g. to create - a password-style textbox containing circular blobs instead of text, - you could set this value to 0x25cf, which is the unicode character - for a black splodge (not all fonts include this, though), or 0x2022, - which is a bullet (probably the best choice for linux). - */ - TextEditor (const String& componentName = String::empty, - const tchar passwordCharacter = 0); - - /** Destructor. */ - virtual ~TextEditor(); - - - //============================================================================== - /** Puts the editor into either multi- or single-line mode. - - By default, the editor will be in single-line mode, so use this if you need a multi-line - editor. - - See also the setReturnKeyStartsNewLine() method, which will also need to be turned - on if you want a multi-line editor with line-breaks. - - @see isMultiLine, setReturnKeyStartsNewLine - */ - void setMultiLine (const bool shouldBeMultiLine, - const bool shouldWordWrap = true); - - /** Returns true if the editor is in multi-line mode. - */ - bool isMultiLine() const throw(); - - //============================================================================== - /** Changes the behaviour of the return key. - - If set to true, the return key will insert a new-line into the text; if false - it will trigger a call to the TextEditorListener::textEditorReturnKeyPressed() - method. By default this is set to false, and when true it will only insert - new-lines when in multi-line mode (see setMultiLine()). - */ - void setReturnKeyStartsNewLine (const bool shouldStartNewLine); - - /** Returns the value set by setReturnKeyStartsNewLine(). - - See setReturnKeyStartsNewLine() for more info. - */ - bool getReturnKeyStartsNewLine() const throw() { return returnKeyStartsNewLine; } - - /** Indicates whether the tab key should be accepted and used to input a tab character, - or whether it gets ignored. - - By default the tab key is ignored, so that it can be used to switch keyboard focus - between components. - */ - void setTabKeyUsedAsCharacter (const bool shouldTabKeyBeUsed) throw(); - - /** Returns true if the tab key is being used for input. - @see setTabKeyUsedAsCharacter - */ - bool isTabKeyUsedAsCharacter() const throw() { return tabKeyUsed; } - - //============================================================================== - /** Changes the editor to read-only mode. - - By default, the text editor is not read-only. If you're making it read-only, you - might also want to call setCaretVisible (false) to get rid of the caret. - - The text can still be highlighted and copied when in read-only mode. - - @see isReadOnly, setCaretVisible - */ - void setReadOnly (const bool shouldBeReadOnly); - - /** Returns true if the editor is in read-only mode. - */ - bool isReadOnly() const throw(); - - //============================================================================== - /** Makes the caret visible or invisible. - - By default the caret is visible. - - @see setCaretColour, setCaretPosition - */ - void setCaretVisible (const bool shouldBeVisible) throw(); - - /** Returns true if the caret is enabled. - @see setCaretVisible - */ - bool isCaretVisible() const throw() { return caretVisible; } - - //============================================================================== - /** Enables/disables a vertical scrollbar. - - (This only applies when in multi-line mode). When the text gets too long to fit - in the component, a scrollbar can appear to allow it to be scrolled. Even when - this is enabled, the scrollbar will be hidden unless it's needed. - - By default the scrollbar is enabled. - */ - void setScrollbarsShown (bool shouldBeEnabled) throw(); - - /** Returns true if scrollbars are enabled. - @see setScrollbarsShown - */ - bool areScrollbarsShown() const throw() { return scrollbarVisible; } - - - /** Changes the password character used to disguise the text. - - @param passwordCharacter if this is not zero, this character will be used as a replacement - for all characters that are drawn on screen - e.g. to create - a password-style textbox containing circular blobs instead of text, - you could set this value to 0x25cf, which is the unicode character - for a black splodge (not all fonts include this, though), or 0x2022, - which is a bullet (probably the best choice for linux). - */ - void setPasswordCharacter (const tchar passwordCharacter) throw(); - - /** Returns the current password character. - @see setPasswordCharacter -l */ - tchar getPasswordCharacter() const throw() { return passwordCharacter; } - - - //============================================================================== - /** Allows a right-click menu to appear for the editor. - - (This defaults to being enabled). - - If enabled, right-clicking (or command-clicking on the Mac) will pop up a menu - of options such as cut/copy/paste, undo/redo, etc. - */ - void setPopupMenuEnabled (const bool menuEnabled) throw(); - - /** Returns true if the right-click menu is enabled. - @see setPopupMenuEnabled - */ - bool isPopupMenuEnabled() const throw() { return popupMenuEnabled; } - - /** Returns true if a popup-menu is currently being displayed. - */ - bool isPopupMenuCurrentlyActive() const throw() { return menuActive; } - - //============================================================================== - /** A set of colour IDs to use to change the colour of various aspects of the editor. - - These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() - methods. - - @see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour - */ - enum ColourIds - { - backgroundColourId = 0x1000200, /**< The colour to use for the text component's background - this can be - transparent if necessary. */ - - textColourId = 0x1000201, /**< The colour that will be used when text is added to the editor. Note - that because the editor can contain multiple colours, calling this - method won't change the colour of existing text - to do that, call - applyFontToAllText() after calling this method.*/ - - highlightColourId = 0x1000202, /**< The colour with which to fill the background of highlighted sections of - the text - this can be transparent if you don't want to show any - highlighting.*/ - - highlightedTextColourId = 0x1000203, /**< The colour with which to draw the text in highlighted sections. */ - - caretColourId = 0x1000204, /**< The colour with which to draw the caret. */ - - outlineColourId = 0x1000205, /**< If this is non-transparent, it will be used to draw a box around - the edge of the component. */ - - focusedOutlineColourId = 0x1000206, /**< If this is non-transparent, it will be used to draw a box around - the edge of the component when it has focus. */ - - shadowColourId = 0x1000207, /**< If this is non-transparent, it'll be used to draw an inner shadow - around the edge of the editor. */ - }; - - //============================================================================== - /** Sets the font to use for newly added text. - - This will change the font that will be used next time any text is added or entered - into the editor. It won't change the font of any existing text - to do that, use - applyFontToAllText() instead. - - @see applyFontToAllText - */ - void setFont (const Font& newFont) throw(); - - /** Applies a font to all the text in the editor. - - This will also set the current font to use for any new text that's added. - - @see setFont - */ - void applyFontToAllText (const Font& newFont); - - /** Returns the font that's currently being used for new text. - - @see setFont - */ - const Font getFont() const throw(); - - //============================================================================== - /** If set to true, focusing on the editor will highlight all its text. - - (Set to false by default). - - This is useful for boxes where you expect the user to re-enter all the - text when they focus on the component, rather than editing what's already there. - */ - void setSelectAllWhenFocused (const bool b) throw(); - - /** Sets limits on the characters that can be entered. - - @param maxTextLength if this is > 0, it sets a maximum length limit; if 0, no - limit is set - @param allowedCharacters if this is non-empty, then only characters that occur in - this string are allowed to be entered into the editor. - */ - void setInputRestrictions (const int maxTextLength, - const String& allowedCharacters = String::empty) throw(); - - /** When the text editor is empty, it can be set to display a message. - - This is handy for things like telling the user what to type in the box - the - string is only displayed, it's not taken to actually be the contents of - the editor. - */ - void setTextToShowWhenEmpty (const String& text, const Colour& colourToUse) throw(); - - //============================================================================== - /** Changes the size of the scrollbars that are used. - - Handy if you need smaller scrollbars for a small text box. - */ - void setScrollBarThickness (const int newThicknessPixels); - - /** Shows or hides the buttons on any scrollbars that are used. - - @see ScrollBar::setButtonVisibility - */ - void setScrollBarButtonVisibility (const bool buttonsVisible); - - //============================================================================== - /** Registers a listener to be told when things happen to the text. - - @see removeListener - */ - void addListener (TextEditorListener* const newListener) throw(); - - /** Deregisters a listener. - - @see addListener - */ - void removeListener (TextEditorListener* const listenerToRemove) throw(); - - //============================================================================== - /** Returns the entire contents of the editor. */ - const String getText() const throw(); - - /** Returns a section of the contents of the editor. */ - const String getTextSubstring (const int startCharacter, const int endCharacter) const throw(); - - /** Returns true if there are no characters in the editor. - - This is more efficient than calling getText().isEmpty(). - */ - bool isEmpty() const throw(); - - /** Sets the entire content of the editor. - - This will clear the editor and insert the given text (using the current text colour - and font). You can set the current text colour using - @code setColour (TextEditor::textColourId, ...); - @endcode - - @param newText the text to add - @param sendTextChangeMessage if true, this will cause a change message to - be sent to all the listeners. - @see insertText - */ - void setText (const String& newText, - const bool sendTextChangeMessage = true); - - /** Inserts some text at the current cursor position. - - If a section of the text is highlighted, it will be replaced by - this string, otherwise it will be inserted. - - To delete a section of text, you can use setHighlightedRegion() to - highlight it, and call insertTextAtCursor (String::empty). - - @see setCaretPosition, getCaretPosition, setHighlightedRegion - */ - void insertTextAtCursor (String textToInsert); - - /** Deletes all the text from the editor. */ - void clear(); - - /** Deletes the currently selected region, and puts it on the clipboard. - - @see copy, paste, SystemClipboard - */ - void cut(); - - /** Copies any currently selected region to the clipboard. - - @see cut, paste, SystemClipboard - */ - void copy(); - - /** Pastes the contents of the clipboard into the editor at the cursor position. - - @see cut, copy, SystemClipboard - */ - void paste(); - - //============================================================================== - /** Moves the caret to be in front of a given character. - - @see getCaretPosition - */ - void setCaretPosition (const int newIndex) throw(); - - /** Returns the current index of the caret. - - @see setCaretPosition - */ - int getCaretPosition() const throw(); - - /** Selects a section of the text. - */ - void setHighlightedRegion (int startIndex, - int numberOfCharactersToHighlight) throw(); - - /** Returns the first character that is selected. - - If nothing is selected, this will still return a character index, but getHighlightedRegionLength() - will return 0. - - @see setHighlightedRegion, getHighlightedRegionLength - */ - int getHighlightedRegionStart() const throw() { return selectionStart; } - - /** Returns the number of characters that are selected. - - @see setHighlightedRegion, getHighlightedRegionStart - */ - int getHighlightedRegionLength() const throw() { return jmax (0, selectionEnd - selectionStart); } - - /** Returns the section of text that is currently selected. */ - const String getHighlightedText() const throw(); - - /** Finds the index of the character at a given position. - - The co-ordinates are relative to the component's top-left. - */ - int getTextIndexAt (const int x, const int y) throw(); - - /** Returns the total width of the text, as it is currently laid-out. - - This may be larger than the size of the TextEditor, and can change when - the TextEditor is resized or the text changes. - */ - int getTextWidth() const throw(); - - /** Returns the maximum height of the text, as it is currently laid-out. - - This may be larger than the size of the TextEditor, and can change when - the TextEditor is resized or the text changes. - */ - int getTextHeight() const throw(); - - /** Changes the size of the gap at the top and left-edge of the editor. - - By default there's a gap of 4 pixels. - */ - void setIndents (const int newLeftIndent, const int newTopIndent) throw(); - - /** Changes the size of border left around the edge of the component. - - @see getBorder - */ - void setBorder (const BorderSize& border) throw(); - - /** Returns the size of border around the edge of the component. - - @see setBorder - */ - const BorderSize getBorder() const throw(); - - /** Used to disable the auto-scrolling which keeps the cursor visible. - - If true (the default), the editor will scroll when the cursor moves offscreen. If - set to false, it won't. - */ - void setScrollToShowCursor (const bool shouldScrollToShowCursor) throw(); - - //============================================================================== - /** @internal */ - void paint (Graphics& g); - /** @internal */ - void paintOverChildren (Graphics& g); - /** @internal */ - void mouseDown (const MouseEvent& e); - /** @internal */ - void mouseUp (const MouseEvent& e); - /** @internal */ - void mouseDrag (const MouseEvent& e); - /** @internal */ - void mouseDoubleClick (const MouseEvent& e); - /** @internal */ - void mouseWheelMove (const MouseEvent& e, float wheelIncrementX, float wheelIncrementY); - /** @internal */ - bool keyPressed (const KeyPress& key); - /** @internal */ - bool keyStateChanged(); - /** @internal */ - void focusGained (FocusChangeType cause); - /** @internal */ - void focusLost (FocusChangeType cause); - /** @internal */ - void resized(); - /** @internal */ - void enablementChanged(); - /** @internal */ - void colourChanged(); - - juce_UseDebuggingNewOperator - -protected: - //============================================================================== - /** This adds the items to the popup menu. - - By default it adds the cut/copy/paste items, but you can override this if - you need to replace these with your own items. - - If you want to add your own items to the existing ones, you can override this, - call the base class's addPopupMenuItems() method, then append your own items. - - When the menu has been shown, performPopupMenuAction() will be called to - perform the item that the user has chosen. - - The default menu items will be added using item IDs in the range - 0x7fff0000 - 0x7fff1000, so you should avoid those values for your own - menu IDs. - - If this was triggered by a mouse-click, the mouseClickEvent parameter will be - a pointer to the info about it, or may be null if the menu is being triggered - by some other means. - - @see performPopupMenuAction, setPopupMenuEnabled, isPopupMenuEnabled - */ - virtual void addPopupMenuItems (PopupMenu& menuToAddTo, - const MouseEvent* mouseClickEvent); - - /** This is called to perform one of the items that was shown on the popup menu. - - If you've overridden addPopupMenuItems(), you should also override this - to perform the actions that you've added. - - If you've overridden addPopupMenuItems() but have still left the default items - on the menu, remember to call the superclass's performPopupMenuAction() - so that it can perform the default actions if that's what the user clicked on. - - @see addPopupMenuItems, setPopupMenuEnabled, isPopupMenuEnabled - */ - virtual void performPopupMenuAction (const int menuItemID); - - //============================================================================== - /** Scrolls the minimum distance needed to get the caret into view. */ - void scrollToMakeSureCursorIsVisible() throw(); - - /** @internal */ - void moveCaret (int newCaretPos) throw(); - - /** @internal */ - void moveCursorTo (const int newPosition, const bool isSelecting) throw(); - - /** Used internally to dispatch a text-change message. */ - void textChanged() throw(); - - /** Counts the number of characters in the text. - - This is quicker than getting the text as a string if you just need to know - the length. - */ - int getTotalNumChars() throw(); - - /** Begins a new transaction in the UndoManager. - */ - void newTransaction() throw(); - - /** Used internally to trigger an undo or redo. */ - void doUndoRedo (const bool isRedo); - - /** Can be overridden to intercept return key presses directly */ - virtual void returnPressed(); - - /** Can be overridden to intercept escape key presses directly */ - virtual void escapePressed(); - - /** @internal */ - void handleCommandMessage (int commandId); - -private: - //============================================================================== - Viewport* viewport; - TextHolderComponent* textHolder; - BorderSize borderSize; - - bool readOnly : 1; - bool multiline : 1; - bool wordWrap : 1; - bool returnKeyStartsNewLine : 1; - bool caretVisible : 1; - bool popupMenuEnabled : 1; - bool selectAllTextWhenFocused : 1; - bool scrollbarVisible : 1; - bool wasFocused : 1; - bool caretFlashState : 1; - bool keepCursorOnScreen : 1; - bool tabKeyUsed : 1; - bool menuActive : 1; - - UndoManager undoManager; - float cursorX, cursorY, cursorHeight; - int maxTextLength; - int selectionStart, selectionEnd; - int leftIndent, topIndent; - unsigned int lastTransactionTime; - Font currentFont; - int totalNumChars, caretPosition; - VoidArray sections; - String textToShowWhenEmpty; - Colour colourForTextWhenEmpty; - tchar passwordCharacter; - - enum - { - notDragging, - draggingSelectionStart, - draggingSelectionEnd - } dragType; - - String allowedCharacters; - SortedSet listeners; - - friend class TextEditorInsertAction; - friend class TextEditorRemoveAction; - - void coalesceSimilarSections() throw(); - void splitSection (const int sectionIndex, const int charToSplitAt) throw(); - - void clearInternal (UndoManager* const um) throw(); - - void insert (const String& text, - const int insertIndex, - const Font& font, - const Colour& colour, - UndoManager* const um, - const int caretPositionToMoveTo) throw(); - - void reinsert (const int insertIndex, - const VoidArray& sections) throw(); - - void remove (const int startIndex, - int endIndex, - UndoManager* const um, - const int caretPositionToMoveTo) throw(); - - void getCharPosition (const int index, - float& x, float& y, - float& lineHeight) const throw(); - - int indexAtPosition (const float x, - const float y) throw(); - - int findWordBreakAfter (const int position) const throw(); - int findWordBreakBefore (const int position) const throw(); - - friend class TextHolderComponent; - friend class TextEditorViewport; - void drawContent (Graphics& g); - void updateTextHolderSize() throw(); - float getWordWrapWidth() const throw(); - void timerCallbackInt(); - void repaintCaret(); - void repaintText (int textStartIndex, int textEndIndex); - - TextEditor (const TextEditor&); - const TextEditor& operator= (const TextEditor&); -}; - -#endif // __JUCE_TEXTEDITOR_JUCEHEADER__ +/* + ============================================================================== + + This file is part of the JUCE library - "Jules' Utility Class Extensions" + Copyright 2004-7 by Raw Material Software ltd. + + ------------------------------------------------------------------------------ + + JUCE can be redistributed and/or modified 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. + + JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA + + ------------------------------------------------------------------------------ + + If you'd like to release a closed-source product which uses JUCE, commercial + licenses are also available: visit www.rawmaterialsoftware.com/juce for + more information. + + ============================================================================== +*/ + +#ifndef __JUCE_TEXTEDITOR_JUCEHEADER__ +#define __JUCE_TEXTEDITOR_JUCEHEADER__ + +#include "../juce_Component.h" +#include "../../../events/juce_Timer.h" +#include "../../../documents/juce_UndoManager.h" +#include "../layout/juce_Viewport.h" +#include "../menus/juce_PopupMenu.h" +class TextEditor; +class TextHolderComponent; + + +//============================================================================== +/** + Receives callbacks from a TextEditor component when it changes. + + @see TextEditor::addListener +*/ +class JUCE_API TextEditorListener +{ +public: + /** Destructor. */ + virtual ~TextEditorListener() {} + + /** Called when the user changes the text in some way. */ + virtual void textEditorTextChanged (TextEditor& editor) = 0; + + /** Called when the user presses the return key. */ + virtual void textEditorReturnKeyPressed (TextEditor& editor) = 0; + + /** Called when the user presses the escape key. */ + virtual void textEditorEscapeKeyPressed (TextEditor& editor) = 0; + + /** Called when the text editor loses focus. */ + virtual void textEditorFocusLost (TextEditor& editor) = 0; +}; + + +//============================================================================== +/** + A component containing text that can be edited. + + A TextEditor can either be in single- or multi-line mode, and supports mixed + fonts and colours. + + @see TextEditorListener, Label +*/ +class JUCE_API TextEditor : public Component, + public SettableTooltipClient +{ +public: + //============================================================================== + /** Creates a new, empty text editor. + + @param componentName the name to pass to the component for it to use as its name + @param passwordCharacter if this is not zero, this character will be used as a replacement + for all characters that are drawn on screen - e.g. to create + a password-style textbox containing circular blobs instead of text, + you could set this value to 0x25cf, which is the unicode character + for a black splodge (not all fonts include this, though), or 0x2022, + which is a bullet (probably the best choice for linux). + */ + TextEditor (const String& componentName = String::empty, + const tchar passwordCharacter = 0); + + /** Destructor. */ + virtual ~TextEditor(); + + + //============================================================================== + /** Puts the editor into either multi- or single-line mode. + + By default, the editor will be in single-line mode, so use this if you need a multi-line + editor. + + See also the setReturnKeyStartsNewLine() method, which will also need to be turned + on if you want a multi-line editor with line-breaks. + + @see isMultiLine, setReturnKeyStartsNewLine + */ + void setMultiLine (const bool shouldBeMultiLine, + const bool shouldWordWrap = true); + + /** Returns true if the editor is in multi-line mode. + */ + bool isMultiLine() const throw(); + + //============================================================================== + /** Changes the behaviour of the return key. + + If set to true, the return key will insert a new-line into the text; if false + it will trigger a call to the TextEditorListener::textEditorReturnKeyPressed() + method. By default this is set to false, and when true it will only insert + new-lines when in multi-line mode (see setMultiLine()). + */ + void setReturnKeyStartsNewLine (const bool shouldStartNewLine); + + /** Returns the value set by setReturnKeyStartsNewLine(). + + See setReturnKeyStartsNewLine() for more info. + */ + bool getReturnKeyStartsNewLine() const throw() { return returnKeyStartsNewLine; } + + /** Indicates whether the tab key should be accepted and used to input a tab character, + or whether it gets ignored. + + By default the tab key is ignored, so that it can be used to switch keyboard focus + between components. + */ + void setTabKeyUsedAsCharacter (const bool shouldTabKeyBeUsed) throw(); + + /** Returns true if the tab key is being used for input. + @see setTabKeyUsedAsCharacter + */ + bool isTabKeyUsedAsCharacter() const throw() { return tabKeyUsed; } + + //============================================================================== + /** Changes the editor to read-only mode. + + By default, the text editor is not read-only. If you're making it read-only, you + might also want to call setCaretVisible (false) to get rid of the caret. + + The text can still be highlighted and copied when in read-only mode. + + @see isReadOnly, setCaretVisible + */ + void setReadOnly (const bool shouldBeReadOnly); + + /** Returns true if the editor is in read-only mode. + */ + bool isReadOnly() const throw(); + + //============================================================================== + /** Makes the caret visible or invisible. + + By default the caret is visible. + + @see setCaretColour, setCaretPosition + */ + void setCaretVisible (const bool shouldBeVisible) throw(); + + /** Returns true if the caret is enabled. + @see setCaretVisible + */ + bool isCaretVisible() const throw() { return caretVisible; } + + //============================================================================== + /** Enables/disables a vertical scrollbar. + + (This only applies when in multi-line mode). When the text gets too long to fit + in the component, a scrollbar can appear to allow it to be scrolled. Even when + this is enabled, the scrollbar will be hidden unless it's needed. + + By default the scrollbar is enabled. + */ + void setScrollbarsShown (bool shouldBeEnabled) throw(); + + /** Returns true if scrollbars are enabled. + @see setScrollbarsShown + */ + bool areScrollbarsShown() const throw() { return scrollbarVisible; } + + + /** Changes the password character used to disguise the text. + + @param passwordCharacter if this is not zero, this character will be used as a replacement + for all characters that are drawn on screen - e.g. to create + a password-style textbox containing circular blobs instead of text, + you could set this value to 0x25cf, which is the unicode character + for a black splodge (not all fonts include this, though), or 0x2022, + which is a bullet (probably the best choice for linux). + */ + void setPasswordCharacter (const tchar passwordCharacter) throw(); + + /** Returns the current password character. + @see setPasswordCharacter +l */ + tchar getPasswordCharacter() const throw() { return passwordCharacter; } + + + //============================================================================== + /** Allows a right-click menu to appear for the editor. + + (This defaults to being enabled). + + If enabled, right-clicking (or command-clicking on the Mac) will pop up a menu + of options such as cut/copy/paste, undo/redo, etc. + */ + void setPopupMenuEnabled (const bool menuEnabled) throw(); + + /** Returns true if the right-click menu is enabled. + @see setPopupMenuEnabled + */ + bool isPopupMenuEnabled() const throw() { return popupMenuEnabled; } + + /** Returns true if a popup-menu is currently being displayed. + */ + bool isPopupMenuCurrentlyActive() const throw() { return menuActive; } + + //============================================================================== + /** A set of colour IDs to use to change the colour of various aspects of the editor. + + These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() + methods. + + @see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour + */ + enum ColourIds + { + backgroundColourId = 0x1000200, /**< The colour to use for the text component's background - this can be + transparent if necessary. */ + + textColourId = 0x1000201, /**< The colour that will be used when text is added to the editor. Note + that because the editor can contain multiple colours, calling this + method won't change the colour of existing text - to do that, call + applyFontToAllText() after calling this method.*/ + + highlightColourId = 0x1000202, /**< The colour with which to fill the background of highlighted sections of + the text - this can be transparent if you don't want to show any + highlighting.*/ + + highlightedTextColourId = 0x1000203, /**< The colour with which to draw the text in highlighted sections. */ + + caretColourId = 0x1000204, /**< The colour with which to draw the caret. */ + + outlineColourId = 0x1000205, /**< If this is non-transparent, it will be used to draw a box around + the edge of the component. */ + + focusedOutlineColourId = 0x1000206, /**< If this is non-transparent, it will be used to draw a box around + the edge of the component when it has focus. */ + + shadowColourId = 0x1000207, /**< If this is non-transparent, it'll be used to draw an inner shadow + around the edge of the editor. */ + }; + + //============================================================================== + /** Sets the font to use for newly added text. + + This will change the font that will be used next time any text is added or entered + into the editor. It won't change the font of any existing text - to do that, use + applyFontToAllText() instead. + + @see applyFontToAllText + */ + void setFont (const Font& newFont) throw(); + + /** Applies a font to all the text in the editor. + + This will also set the current font to use for any new text that's added. + + @see setFont + */ + void applyFontToAllText (const Font& newFont); + + /** Returns the font that's currently being used for new text. + + @see setFont + */ + const Font getFont() const throw(); + + //============================================================================== + /** If set to true, focusing on the editor will highlight all its text. + + (Set to false by default). + + This is useful for boxes where you expect the user to re-enter all the + text when they focus on the component, rather than editing what's already there. + */ + void setSelectAllWhenFocused (const bool b) throw(); + + /** Sets limits on the characters that can be entered. + + @param maxTextLength if this is > 0, it sets a maximum length limit; if 0, no + limit is set + @param allowedCharacters if this is non-empty, then only characters that occur in + this string are allowed to be entered into the editor. + */ + void setInputRestrictions (const int maxTextLength, + const String& allowedCharacters = String::empty) throw(); + + /** When the text editor is empty, it can be set to display a message. + + This is handy for things like telling the user what to type in the box - the + string is only displayed, it's not taken to actually be the contents of + the editor. + */ + void setTextToShowWhenEmpty (const String& text, const Colour& colourToUse) throw(); + + //============================================================================== + /** Changes the size of the scrollbars that are used. + + Handy if you need smaller scrollbars for a small text box. + */ + void setScrollBarThickness (const int newThicknessPixels); + + /** Shows or hides the buttons on any scrollbars that are used. + + @see ScrollBar::setButtonVisibility + */ + void setScrollBarButtonVisibility (const bool buttonsVisible); + + //============================================================================== + /** Registers a listener to be told when things happen to the text. + + @see removeListener + */ + void addListener (TextEditorListener* const newListener) throw(); + + /** Deregisters a listener. + + @see addListener + */ + void removeListener (TextEditorListener* const listenerToRemove) throw(); + + //============================================================================== + /** Returns the entire contents of the editor. */ + const String getText() const throw(); + + /** Returns a section of the contents of the editor. */ + const String getTextSubstring (const int startCharacter, const int endCharacter) const throw(); + + /** Returns true if there are no characters in the editor. + + This is more efficient than calling getText().isEmpty(). + */ + bool isEmpty() const throw(); + + /** Sets the entire content of the editor. + + This will clear the editor and insert the given text (using the current text colour + and font). You can set the current text colour using + @code setColour (TextEditor::textColourId, ...); + @endcode + + @param newText the text to add + @param sendTextChangeMessage if true, this will cause a change message to + be sent to all the listeners. + @see insertText + */ + void setText (const String& newText, + const bool sendTextChangeMessage = true); + + /** Inserts some text at the current cursor position. + + If a section of the text is highlighted, it will be replaced by + this string, otherwise it will be inserted. + + To delete a section of text, you can use setHighlightedRegion() to + highlight it, and call insertTextAtCursor (String::empty). + + @see setCaretPosition, getCaretPosition, setHighlightedRegion + */ + void insertTextAtCursor (String textToInsert); + + /** Deletes all the text from the editor. */ + void clear(); + + /** Deletes the currently selected region, and puts it on the clipboard. + + @see copy, paste, SystemClipboard + */ + void cut(); + + /** Copies any currently selected region to the clipboard. + + @see cut, paste, SystemClipboard + */ + void copy(); + + /** Pastes the contents of the clipboard into the editor at the cursor position. + + @see cut, copy, SystemClipboard + */ + void paste(); + + //============================================================================== + /** Moves the caret to be in front of a given character. + + @see getCaretPosition + */ + void setCaretPosition (const int newIndex) throw(); + + /** Returns the current index of the caret. + + @see setCaretPosition + */ + int getCaretPosition() const throw(); + + /** Attempts to scroll the text editor so that the caret ends up at + a specified position. + + This won't affect the caret's position within the text, it tries to scroll + the entire editor vertically and horizontally so that the caret is sitting + at the given position (relative to the top-left of this component). + + Depending on the amount of text available, it might not be possible to + scroll far enough for the caret to reach this exact position, but it + will go as far as it can in that direction. + */ + void scrollEditorToPositionCaret (const int desiredCaretX, + const int desiredCaretY) throw(); + + /** Get the graphical position of the caret. + + The rectangle returned is relative to the component's top-left corner. + @see scrollEditorToPositionCaret + */ + const Rectangle getCaretRectangle() throw(); + + /** Selects a section of the text. + */ + void setHighlightedRegion (int startIndex, + int numberOfCharactersToHighlight) throw(); + + /** Returns the first character that is selected. + + If nothing is selected, this will still return a character index, but getHighlightedRegionLength() + will return 0. + + @see setHighlightedRegion, getHighlightedRegionLength + */ + int getHighlightedRegionStart() const throw() { return selectionStart; } + + /** Returns the number of characters that are selected. + + @see setHighlightedRegion, getHighlightedRegionStart + */ + int getHighlightedRegionLength() const throw() { return jmax (0, selectionEnd - selectionStart); } + + /** Returns the section of text that is currently selected. */ + const String getHighlightedText() const throw(); + + /** Finds the index of the character at a given position. + + The co-ordinates are relative to the component's top-left. + */ + int getTextIndexAt (const int x, const int y) throw(); + + /** Returns the total width of the text, as it is currently laid-out. + + This may be larger than the size of the TextEditor, and can change when + the TextEditor is resized or the text changes. + */ + int getTextWidth() const throw(); + + /** Returns the maximum height of the text, as it is currently laid-out. + + This may be larger than the size of the TextEditor, and can change when + the TextEditor is resized or the text changes. + */ + int getTextHeight() const throw(); + + /** Changes the size of the gap at the top and left-edge of the editor. + + By default there's a gap of 4 pixels. + */ + void setIndents (const int newLeftIndent, const int newTopIndent) throw(); + + /** Changes the size of border left around the edge of the component. + + @see getBorder + */ + void setBorder (const BorderSize& border) throw(); + + /** Returns the size of border around the edge of the component. + + @see setBorder + */ + const BorderSize getBorder() const throw(); + + /** Used to disable the auto-scrolling which keeps the cursor visible. + + If true (the default), the editor will scroll when the cursor moves offscreen. If + set to false, it won't. + */ + void setScrollToShowCursor (const bool shouldScrollToShowCursor) throw(); + + //============================================================================== + /** @internal */ + void paint (Graphics& g); + /** @internal */ + void paintOverChildren (Graphics& g); + /** @internal */ + void mouseDown (const MouseEvent& e); + /** @internal */ + void mouseUp (const MouseEvent& e); + /** @internal */ + void mouseDrag (const MouseEvent& e); + /** @internal */ + void mouseDoubleClick (const MouseEvent& e); + /** @internal */ + void mouseWheelMove (const MouseEvent& e, float wheelIncrementX, float wheelIncrementY); + /** @internal */ + bool keyPressed (const KeyPress& key); + /** @internal */ + bool keyStateChanged(); + /** @internal */ + void focusGained (FocusChangeType cause); + /** @internal */ + void focusLost (FocusChangeType cause); + /** @internal */ + void resized(); + /** @internal */ + void enablementChanged(); + /** @internal */ + void colourChanged(); + + juce_UseDebuggingNewOperator + +protected: + //============================================================================== + /** This adds the items to the popup menu. + + By default it adds the cut/copy/paste items, but you can override this if + you need to replace these with your own items. + + If you want to add your own items to the existing ones, you can override this, + call the base class's addPopupMenuItems() method, then append your own items. + + When the menu has been shown, performPopupMenuAction() will be called to + perform the item that the user has chosen. + + The default menu items will be added using item IDs in the range + 0x7fff0000 - 0x7fff1000, so you should avoid those values for your own + menu IDs. + + If this was triggered by a mouse-click, the mouseClickEvent parameter will be + a pointer to the info about it, or may be null if the menu is being triggered + by some other means. + + @see performPopupMenuAction, setPopupMenuEnabled, isPopupMenuEnabled + */ + virtual void addPopupMenuItems (PopupMenu& menuToAddTo, + const MouseEvent* mouseClickEvent); + + /** This is called to perform one of the items that was shown on the popup menu. + + If you've overridden addPopupMenuItems(), you should also override this + to perform the actions that you've added. + + If you've overridden addPopupMenuItems() but have still left the default items + on the menu, remember to call the superclass's performPopupMenuAction() + so that it can perform the default actions if that's what the user clicked on. + + @see addPopupMenuItems, setPopupMenuEnabled, isPopupMenuEnabled + */ + virtual void performPopupMenuAction (const int menuItemID); + + //============================================================================== + /** Scrolls the minimum distance needed to get the caret into view. */ + void scrollToMakeSureCursorIsVisible() throw(); + + /** @internal */ + void moveCaret (int newCaretPos) throw(); + + /** @internal */ + void moveCursorTo (const int newPosition, const bool isSelecting) throw(); + + /** Used internally to dispatch a text-change message. */ + void textChanged() throw(); + + /** Counts the number of characters in the text. + + This is quicker than getting the text as a string if you just need to know + the length. + */ + int getTotalNumChars() throw(); + + /** Begins a new transaction in the UndoManager. + */ + void newTransaction() throw(); + + /** Used internally to trigger an undo or redo. */ + void doUndoRedo (const bool isRedo); + + /** Can be overridden to intercept return key presses directly */ + virtual void returnPressed(); + + /** Can be overridden to intercept escape key presses directly */ + virtual void escapePressed(); + + /** @internal */ + void handleCommandMessage (int commandId); + +private: + //============================================================================== + Viewport* viewport; + TextHolderComponent* textHolder; + BorderSize borderSize; + + bool readOnly : 1; + bool multiline : 1; + bool wordWrap : 1; + bool returnKeyStartsNewLine : 1; + bool caretVisible : 1; + bool popupMenuEnabled : 1; + bool selectAllTextWhenFocused : 1; + bool scrollbarVisible : 1; + bool wasFocused : 1; + bool caretFlashState : 1; + bool keepCursorOnScreen : 1; + bool tabKeyUsed : 1; + bool menuActive : 1; + + UndoManager undoManager; + float cursorX, cursorY, cursorHeight; + int maxTextLength; + int selectionStart, selectionEnd; + int leftIndent, topIndent; + unsigned int lastTransactionTime; + Font currentFont; + int totalNumChars, caretPosition; + VoidArray sections; + String textToShowWhenEmpty; + Colour colourForTextWhenEmpty; + tchar passwordCharacter; + + enum + { + notDragging, + draggingSelectionStart, + draggingSelectionEnd + } dragType; + + String allowedCharacters; + SortedSet listeners; + + friend class TextEditorInsertAction; + friend class TextEditorRemoveAction; + + void coalesceSimilarSections() throw(); + void splitSection (const int sectionIndex, const int charToSplitAt) throw(); + + void clearInternal (UndoManager* const um) throw(); + + void insert (const String& text, + const int insertIndex, + const Font& font, + const Colour& colour, + UndoManager* const um, + const int caretPositionToMoveTo) throw(); + + void reinsert (const int insertIndex, + const VoidArray& sections) throw(); + + void remove (const int startIndex, + int endIndex, + UndoManager* const um, + const int caretPositionToMoveTo) throw(); + + void getCharPosition (const int index, + float& x, float& y, + float& lineHeight) const throw(); + + void updateCaretPosition() throw(); + + int indexAtPosition (const float x, + const float y) throw(); + + int findWordBreakAfter (const int position) const throw(); + int findWordBreakBefore (const int position) const throw(); + + friend class TextHolderComponent; + friend class TextEditorViewport; + void drawContent (Graphics& g); + void updateTextHolderSize() throw(); + float getWordWrapWidth() const throw(); + void timerCallbackInt(); + void repaintCaret(); + void repaintText (int textStartIndex, int textEndIndex); + + TextEditor (const TextEditor&); + const TextEditor& operator= (const TextEditor&); +}; + +#endif // __JUCE_TEXTEDITOR_JUCEHEADER__ diff --git a/src/juce_appframework/gui/components/properties/juce_PropertyPanel.cpp b/src/juce_appframework/gui/components/properties/juce_PropertyPanel.cpp index 97a8f79aa2..074fe11092 100644 --- a/src/juce_appframework/gui/components/properties/juce_PropertyPanel.cpp +++ b/src/juce_appframework/gui/components/properties/juce_PropertyPanel.cpp @@ -1,421 +1,442 @@ -/* - ============================================================================== - - This file is part of the JUCE library - "Jules' Utility Class Extensions" - Copyright 2004-7 by Raw Material Software ltd. - - ------------------------------------------------------------------------------ - - JUCE can be redistributed and/or modified 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. - - JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the - Free Software Foundation, Inc., 59 Temple Place, Suite 330, - Boston, MA 02111-1307 USA - - ------------------------------------------------------------------------------ - - If you'd like to release a closed-source product which uses JUCE, commercial - licenses are also available: visit www.rawmaterialsoftware.com/juce for - more information. - - ============================================================================== -*/ - -#include "../../../../juce_core/basics/juce_StandardHeader.h" - -BEGIN_JUCE_NAMESPACE - -#include "juce_PropertyPanel.h" -#include "../lookandfeel/juce_LookAndFeel.h" -#include "../../../../juce_core/text/juce_LocalisedStrings.h" - - -//============================================================================== -class PropertyHolderComponent : public Component -{ -public: - PropertyHolderComponent() - { - } - - ~PropertyHolderComponent() - { - deleteAllChildren(); - } - - void paint (Graphics&) - { - } - - void updateLayout (const int width); - - void refreshAll() const; -}; - -//============================================================================== -class PropertySectionComponent : public Component -{ -public: - PropertySectionComponent (const String& sectionTitle, - const Array & newProperties, - const bool open) - : Component (sectionTitle), - titleHeight (sectionTitle.isNotEmpty() ? 22 : 0), - isOpen_ (open) - { - for (int i = newProperties.size(); --i >= 0;) - { - addAndMakeVisible (newProperties.getUnchecked(i)); - newProperties.getUnchecked(i)->refresh(); - } - } - - ~PropertySectionComponent() - { - deleteAllChildren(); - } - - void paint (Graphics& g) - { - if (titleHeight > 0) - getLookAndFeel().drawPropertyPanelSectionHeader (g, getName(), isOpen(), getWidth(), titleHeight); - } - - void resized() - { - int y = titleHeight; - - for (int i = getNumChildComponents(); --i >= 0;) - { - PropertyComponent* const pec = dynamic_cast (getChildComponent (i)); - - if (pec != 0) - { - const int prefH = pec->getPreferredHeight(); - pec->setBounds (1, y, getWidth() - 2, prefH); - y += prefH; - } - } - } - - int getPreferredHeight() const - { - int y = titleHeight; - - if (isOpen()) - { - for (int i = 0; i < getNumChildComponents(); ++i) - { - PropertyComponent* pec = dynamic_cast (getChildComponent (i)); - - if (pec != 0) - y += pec->getPreferredHeight(); - } - } - - return y; - } - - void setOpen (const bool open) - { - if (isOpen_ != open) - { - isOpen_ = open; - - for (int i = 0; i < getNumChildComponents(); ++i) - { - PropertyComponent* pec = dynamic_cast (getChildComponent (i)); - - if (pec != 0) - pec->setVisible (open); - } - - // (unable to use the syntax findParentComponentOfClass () because of a VC6 compiler bug) - PropertyPanel* const pp = findParentComponentOfClass ((PropertyPanel*) 0); - - if (pp != 0) - pp->resized(); - } - } - - bool isOpen() const throw() - { - return isOpen_; - } - - void refreshAll() const - { - for (int i = 0; i < getNumChildComponents(); ++i) - { - PropertyComponent* pec = dynamic_cast (getChildComponent (i)); - - if (pec != 0) - pec->refresh(); - } - } - - void mouseDown (const MouseEvent&) - { - } - - void mouseUp (const MouseEvent& e) - { - if (e.getMouseDownX() < titleHeight - && e.x < titleHeight - && e.y < titleHeight - && e.getNumberOfClicks() != 2) - { - setOpen (! isOpen()); - } - } - - void mouseDoubleClick (const MouseEvent& e) - { - if (e.y < titleHeight) - setOpen (! isOpen()); - } - -private: - int titleHeight; - bool isOpen_; -}; - -void PropertyHolderComponent::updateLayout (const int width) -{ - int y = 0; - - for (int i = getNumChildComponents(); --i >= 0;) - { - PropertySectionComponent* const section - = dynamic_cast (getChildComponent (i)); - - if (section != 0) - { - const int prefH = section->getPreferredHeight(); - section->setBounds (0, y, width, prefH); - y += prefH; - } - } - - setSize (width, y); - repaint(); -} - -void PropertyHolderComponent::refreshAll() const -{ - for (int i = getNumChildComponents(); --i >= 0;) - { - PropertySectionComponent* const section - = dynamic_cast (getChildComponent (i)); - - if (section != 0) - section->refreshAll(); - } -} - -//============================================================================== -PropertyPanel::PropertyPanel() -{ - messageWhenEmpty = TRANS("(nothing selected)"); - - addAndMakeVisible (viewport = new Viewport()); - viewport->setViewedComponent (propertyHolderComponent = new PropertyHolderComponent()); - viewport->setFocusContainer (true); -} - -PropertyPanel::~PropertyPanel() -{ - clear(); - deleteAllChildren(); -} - -//============================================================================== -void PropertyPanel::paint (Graphics& g) -{ - if (propertyHolderComponent->getNumChildComponents() == 0) - { - g.setColour (Colours::black.withAlpha (0.5f)); - g.setFont (14.0f); - g.drawText (messageWhenEmpty, 0, 0, getWidth(), 30, - Justification::centred, true); - } -} - -void PropertyPanel::resized() -{ - viewport->setBounds (0, 0, getWidth(), getHeight()); - updatePropHolderLayout(); -} - -//============================================================================== -void PropertyPanel::clear() -{ - if (propertyHolderComponent->getNumChildComponents() > 0) - { - propertyHolderComponent->deleteAllChildren(); - repaint(); - } -} - -void PropertyPanel::addProperties (const Array & newProperties) -{ - if (propertyHolderComponent->getNumChildComponents() == 0) - repaint(); - - propertyHolderComponent->addAndMakeVisible (new PropertySectionComponent (String::empty, - newProperties, - true), 0); - updatePropHolderLayout(); -} - -void PropertyPanel::addSection (const String& sectionTitle, - const Array & newProperties, - const bool shouldBeOpen) -{ - jassert (sectionTitle.isNotEmpty()); - - if (propertyHolderComponent->getNumChildComponents() == 0) - repaint(); - - propertyHolderComponent->addAndMakeVisible (new PropertySectionComponent (sectionTitle, - newProperties, - shouldBeOpen), 0); - - updatePropHolderLayout(); -} - -void PropertyPanel::updatePropHolderLayout() const -{ - const int maxWidth = viewport->getMaximumVisibleWidth(); - ((PropertyHolderComponent*) propertyHolderComponent)->updateLayout (maxWidth); - - const int newMaxWidth = viewport->getMaximumVisibleWidth(); - if (maxWidth != newMaxWidth) - { - // need to do this twice because of scrollbars changing the size, etc. - ((PropertyHolderComponent*) propertyHolderComponent)->updateLayout (newMaxWidth); - } -} - -void PropertyPanel::refreshAll() const -{ - ((PropertyHolderComponent*) propertyHolderComponent)->refreshAll(); -} - -//============================================================================== -const StringArray PropertyPanel::getSectionNames() const -{ - StringArray s; - - for (int i = 0; i < propertyHolderComponent->getNumChildComponents(); ++i) - { - PropertySectionComponent* const section = dynamic_cast (propertyHolderComponent->getChildComponent (i)); - - if (section != 0 && section->getName().isNotEmpty()) - s.add (section->getName()); - } - - return s; -} - -bool PropertyPanel::isSectionOpen (const int sectionIndex) const -{ - int index = 0; - - for (int i = 0; i < propertyHolderComponent->getNumChildComponents(); ++i) - { - PropertySectionComponent* const section = dynamic_cast (propertyHolderComponent->getChildComponent (i)); - - if (section != 0 && section->getName().isNotEmpty()) - { - if (index == sectionIndex) - return section->isOpen(); - - ++index; - } - } - - return false; -} - -void PropertyPanel::setSectionOpen (const int sectionIndex, const bool shouldBeOpen) -{ - int index = 0; - - for (int i = 0; i < propertyHolderComponent->getNumChildComponents(); ++i) - { - PropertySectionComponent* const section = dynamic_cast (propertyHolderComponent->getChildComponent (i)); - - if (section != 0 && section->getName().isNotEmpty()) - { - if (index == sectionIndex) - { - section->setOpen (shouldBeOpen); - break; - } - - ++index; - } - } -} - -//============================================================================== -XmlElement* PropertyPanel::getOpennessState() const -{ - XmlElement* const xml = new XmlElement (T("PROPERTYPANELSTATE")); - - const StringArray sections (getSectionNames()); - - for (int i = 0; i < sections.size(); ++i) - { - if (sections[i].isNotEmpty()) - { - XmlElement* const e = new XmlElement (T("SECTION")); - e->setAttribute (T("name"), sections[i]); - e->setAttribute (T("open"), isSectionOpen (i) ? 1 : 0); - xml->addChildElement (e); - } - } - - return xml; -} - -void PropertyPanel::restoreOpennessState (const XmlElement& xml) -{ - if (xml.hasTagName (T("PROPERTYPANELSTATE"))) - { - const StringArray sections (getSectionNames()); - - forEachXmlChildElementWithTagName (xml, e, T("SECTION")) - { - setSectionOpen (sections.indexOf (e->getStringAttribute (T("name"))), - e->getBoolAttribute (T("open"))); - } - } -} - -//============================================================================== -void PropertyPanel::setMessageWhenEmpty (const String& newMessage) -{ - if (messageWhenEmpty != newMessage) - { - messageWhenEmpty = newMessage; - repaint(); - } -} - -const String& PropertyPanel::getMessageWhenEmpty() const throw() -{ - return messageWhenEmpty; -} - - -END_JUCE_NAMESPACE +/* + ============================================================================== + + This file is part of the JUCE library - "Jules' Utility Class Extensions" + Copyright 2004-7 by Raw Material Software ltd. + + ------------------------------------------------------------------------------ + + JUCE can be redistributed and/or modified 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. + + JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA + + ------------------------------------------------------------------------------ + + If you'd like to release a closed-source product which uses JUCE, commercial + licenses are also available: visit www.rawmaterialsoftware.com/juce for + more information. + + ============================================================================== +*/ + +#include "../../../../juce_core/basics/juce_StandardHeader.h" + +BEGIN_JUCE_NAMESPACE + +#include "juce_PropertyPanel.h" +#include "../lookandfeel/juce_LookAndFeel.h" +#include "../../../../juce_core/text/juce_LocalisedStrings.h" + + +//============================================================================== +class PropertyHolderComponent : public Component +{ +public: + PropertyHolderComponent() + { + } + + ~PropertyHolderComponent() + { + deleteAllChildren(); + } + + void paint (Graphics&) + { + } + + void updateLayout (const int width); + + void refreshAll() const; +}; + +//============================================================================== +class PropertySectionComponent : public Component +{ +public: + PropertySectionComponent (const String& sectionTitle, + const Array & newProperties, + const bool open) + : Component (sectionTitle), + titleHeight (sectionTitle.isNotEmpty() ? 22 : 0), + isOpen_ (open) + { + for (int i = newProperties.size(); --i >= 0;) + { + addAndMakeVisible (newProperties.getUnchecked(i)); + newProperties.getUnchecked(i)->refresh(); + } + } + + ~PropertySectionComponent() + { + deleteAllChildren(); + } + + void paint (Graphics& g) + { + if (titleHeight > 0) + getLookAndFeel().drawPropertyPanelSectionHeader (g, getName(), isOpen(), getWidth(), titleHeight); + } + + void resized() + { + int y = titleHeight; + + for (int i = getNumChildComponents(); --i >= 0;) + { + PropertyComponent* const pec = dynamic_cast (getChildComponent (i)); + + if (pec != 0) + { + const int prefH = pec->getPreferredHeight(); + pec->setBounds (1, y, getWidth() - 2, prefH); + y += prefH; + } + } + } + + int getPreferredHeight() const + { + int y = titleHeight; + + if (isOpen()) + { + for (int i = 0; i < getNumChildComponents(); ++i) + { + PropertyComponent* pec = dynamic_cast (getChildComponent (i)); + + if (pec != 0) + y += pec->getPreferredHeight(); + } + } + + return y; + } + + void setOpen (const bool open) + { + if (isOpen_ != open) + { + isOpen_ = open; + + for (int i = 0; i < getNumChildComponents(); ++i) + { + PropertyComponent* pec = dynamic_cast (getChildComponent (i)); + + if (pec != 0) + pec->setVisible (open); + } + + // (unable to use the syntax findParentComponentOfClass () because of a VC6 compiler bug) + PropertyPanel* const pp = findParentComponentOfClass ((PropertyPanel*) 0); + + if (pp != 0) + pp->resized(); + } + } + + bool isOpen() const throw() + { + return isOpen_; + } + + void refreshAll() const + { + for (int i = 0; i < getNumChildComponents(); ++i) + { + PropertyComponent* pec = dynamic_cast (getChildComponent (i)); + + if (pec != 0) + pec->refresh(); + } + } + + void mouseDown (const MouseEvent&) + { + } + + void mouseUp (const MouseEvent& e) + { + if (e.getMouseDownX() < titleHeight + && e.x < titleHeight + && e.y < titleHeight + && e.getNumberOfClicks() != 2) + { + setOpen (! isOpen()); + } + } + + void mouseDoubleClick (const MouseEvent& e) + { + if (e.y < titleHeight) + setOpen (! isOpen()); + } + +private: + int titleHeight; + bool isOpen_; +}; + +void PropertyHolderComponent::updateLayout (const int width) +{ + int y = 0; + + for (int i = getNumChildComponents(); --i >= 0;) + { + PropertySectionComponent* const section + = dynamic_cast (getChildComponent (i)); + + if (section != 0) + { + const int prefH = section->getPreferredHeight(); + section->setBounds (0, y, width, prefH); + y += prefH; + } + } + + setSize (width, y); + repaint(); +} + +void PropertyHolderComponent::refreshAll() const +{ + for (int i = getNumChildComponents(); --i >= 0;) + { + PropertySectionComponent* const section + = dynamic_cast (getChildComponent (i)); + + if (section != 0) + section->refreshAll(); + } +} + +//============================================================================== +PropertyPanel::PropertyPanel() +{ + messageWhenEmpty = TRANS("(nothing selected)"); + + addAndMakeVisible (viewport = new Viewport()); + viewport->setViewedComponent (propertyHolderComponent = new PropertyHolderComponent()); + viewport->setFocusContainer (true); +} + +PropertyPanel::~PropertyPanel() +{ + clear(); + deleteAllChildren(); +} + +//============================================================================== +void PropertyPanel::paint (Graphics& g) +{ + if (propertyHolderComponent->getNumChildComponents() == 0) + { + g.setColour (Colours::black.withAlpha (0.5f)); + g.setFont (14.0f); + g.drawText (messageWhenEmpty, 0, 0, getWidth(), 30, + Justification::centred, true); + } +} + +void PropertyPanel::resized() +{ + viewport->setBounds (0, 0, getWidth(), getHeight()); + updatePropHolderLayout(); +} + +//============================================================================== +void PropertyPanel::clear() +{ + if (propertyHolderComponent->getNumChildComponents() > 0) + { + propertyHolderComponent->deleteAllChildren(); + repaint(); + } +} + +void PropertyPanel::addProperties (const Array & newProperties) +{ + if (propertyHolderComponent->getNumChildComponents() == 0) + repaint(); + + propertyHolderComponent->addAndMakeVisible (new PropertySectionComponent (String::empty, + newProperties, + true), 0); + updatePropHolderLayout(); +} + +void PropertyPanel::addSection (const String& sectionTitle, + const Array & newProperties, + const bool shouldBeOpen) +{ + jassert (sectionTitle.isNotEmpty()); + + if (propertyHolderComponent->getNumChildComponents() == 0) + repaint(); + + propertyHolderComponent->addAndMakeVisible (new PropertySectionComponent (sectionTitle, + newProperties, + shouldBeOpen), 0); + + updatePropHolderLayout(); +} + +void PropertyPanel::updatePropHolderLayout() const +{ + const int maxWidth = viewport->getMaximumVisibleWidth(); + ((PropertyHolderComponent*) propertyHolderComponent)->updateLayout (maxWidth); + + const int newMaxWidth = viewport->getMaximumVisibleWidth(); + if (maxWidth != newMaxWidth) + { + // need to do this twice because of scrollbars changing the size, etc. + ((PropertyHolderComponent*) propertyHolderComponent)->updateLayout (newMaxWidth); + } +} + +void PropertyPanel::refreshAll() const +{ + ((PropertyHolderComponent*) propertyHolderComponent)->refreshAll(); +} + +//============================================================================== +const StringArray PropertyPanel::getSectionNames() const +{ + StringArray s; + + for (int i = 0; i < propertyHolderComponent->getNumChildComponents(); ++i) + { + PropertySectionComponent* const section = dynamic_cast (propertyHolderComponent->getChildComponent (i)); + + if (section != 0 && section->getName().isNotEmpty()) + s.add (section->getName()); + } + + return s; +} + +bool PropertyPanel::isSectionOpen (const int sectionIndex) const +{ + int index = 0; + + for (int i = 0; i < propertyHolderComponent->getNumChildComponents(); ++i) + { + PropertySectionComponent* const section = dynamic_cast (propertyHolderComponent->getChildComponent (i)); + + if (section != 0 && section->getName().isNotEmpty()) + { + if (index == sectionIndex) + return section->isOpen(); + + ++index; + } + } + + return false; +} + +void PropertyPanel::setSectionOpen (const int sectionIndex, const bool shouldBeOpen) +{ + int index = 0; + + for (int i = 0; i < propertyHolderComponent->getNumChildComponents(); ++i) + { + PropertySectionComponent* const section = dynamic_cast (propertyHolderComponent->getChildComponent (i)); + + if (section != 0 && section->getName().isNotEmpty()) + { + if (index == sectionIndex) + { + section->setOpen (shouldBeOpen); + break; + } + + ++index; + } + } +} + +void PropertyPanel::setSectionEnabled (const int sectionIndex, const bool shouldBeEnabled) +{ + int index = 0; + + for (int i = 0; i < propertyHolderComponent->getNumChildComponents(); ++i) + { + PropertySectionComponent* const section = dynamic_cast (propertyHolderComponent->getChildComponent (i)); + + if (section != 0 && section->getName().isNotEmpty()) + { + if (index == sectionIndex) + { + section->setEnabled (shouldBeEnabled); + break; + } + + ++index; + } + } +} + +//============================================================================== +XmlElement* PropertyPanel::getOpennessState() const +{ + XmlElement* const xml = new XmlElement (T("PROPERTYPANELSTATE")); + + const StringArray sections (getSectionNames()); + + for (int i = 0; i < sections.size(); ++i) + { + if (sections[i].isNotEmpty()) + { + XmlElement* const e = new XmlElement (T("SECTION")); + e->setAttribute (T("name"), sections[i]); + e->setAttribute (T("open"), isSectionOpen (i) ? 1 : 0); + xml->addChildElement (e); + } + } + + return xml; +} + +void PropertyPanel::restoreOpennessState (const XmlElement& xml) +{ + if (xml.hasTagName (T("PROPERTYPANELSTATE"))) + { + const StringArray sections (getSectionNames()); + + forEachXmlChildElementWithTagName (xml, e, T("SECTION")) + { + setSectionOpen (sections.indexOf (e->getStringAttribute (T("name"))), + e->getBoolAttribute (T("open"))); + } + } +} + +//============================================================================== +void PropertyPanel::setMessageWhenEmpty (const String& newMessage) +{ + if (messageWhenEmpty != newMessage) + { + messageWhenEmpty = newMessage; + repaint(); + } +} + +const String& PropertyPanel::getMessageWhenEmpty() const throw() +{ + return messageWhenEmpty; +} + + +END_JUCE_NAMESPACE diff --git a/src/juce_appframework/gui/components/properties/juce_PropertyPanel.h b/src/juce_appframework/gui/components/properties/juce_PropertyPanel.h index d8f694dcef..0428a98cf2 100644 --- a/src/juce_appframework/gui/components/properties/juce_PropertyPanel.h +++ b/src/juce_appframework/gui/components/properties/juce_PropertyPanel.h @@ -109,6 +109,12 @@ public: */ void setSectionOpen (const int sectionIndex, const bool shouldBeOpen); + /** Enables or disables one of the sections. + + The index is from 0 up to the number of items returned by getSectionNames(). + */ + void setSectionEnabled (const int sectionIndex, const bool shouldBeEnabled); + //============================================================================== /** Saves the current state of open/closed sections so it can be restored later. diff --git a/src/juce_appframework/gui/graphics/imaging/image_file_formats/juce_JPEGLoader.cpp b/src/juce_appframework/gui/graphics/imaging/image_file_formats/juce_JPEGLoader.cpp index e84a8d06c3..0a0f3766e6 100644 --- a/src/juce_appframework/gui/graphics/imaging/image_file_formats/juce_JPEGLoader.cpp +++ b/src/juce_appframework/gui/graphics/imaging/image_file_formats/juce_JPEGLoader.cpp @@ -1,376 +1,378 @@ -/* - ============================================================================== - - This file is part of the JUCE library - "Jules' Utility Class Extensions" - Copyright 2004-7 by Raw Material Software ltd. - - ------------------------------------------------------------------------------ - - JUCE can be redistributed and/or modified 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. - - JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the - Free Software Foundation, Inc., 59 Temple Place, Suite 330, - Boston, MA 02111-1307 USA - - ------------------------------------------------------------------------------ - - If you'd like to release a closed-source product which uses JUCE, commercial - licenses are also available: visit www.rawmaterialsoftware.com/juce for - more information. - - ============================================================================== -*/ - -#include "../../../../../juce_core/basics/juce_StandardHeader.h" - -#if JUCE_MSVC - #pragma warning (push) -#endif - -namespace jpeglibNamespace -{ - extern "C" - { - #define JPEG_INTERNALS - #undef FAR - #include "jpglib/jpeglib.h" - - #include "jpglib/jcapimin.c" - #include "jpglib/jcapistd.c" - #include "jpglib/jccoefct.c" - #include "jpglib/jccolor.c" - #undef FIX - #include "jpglib/jcdctmgr.c" - #undef CONST_BITS - #include "jpglib/jchuff.c" - #undef emit_byte - #include "jpglib/jcinit.c" - #include "jpglib/jcmainct.c" - #include "jpglib/jcmarker.c" - #include "jpglib/jcmaster.c" - #include "jpglib/jcomapi.c" - #include "jpglib/jcparam.c" - #include "jpglib/jcphuff.c" - #include "jpglib/jcprepct.c" - #include "jpglib/jcsample.c" - #include "jpglib/jctrans.c" - #include "jpglib/jdapistd.c" - #include "jpglib/jdapimin.c" - #include "jpglib/jdatasrc.c" - #include "jpglib/jdcoefct.c" - #undef FIX - #include "jpglib/jdcolor.c" - #undef FIX - #include "jpglib/jddctmgr.c" - #undef CONST_BITS - #undef ASSIGN_STATE - #include "jpglib/jdhuff.c" - #include "jpglib/jdinput.c" - #include "jpglib/jdmainct.c" - #include "jpglib/jdmarker.c" - #include "jpglib/jdmaster.c" - #undef FIX - #include "jpglib/jdmerge.c" - #undef ASSIGN_STATE - #include "jpglib/jdphuff.c" - #include "jpglib/jdpostct.c" - #undef FIX - #include "jpglib/jdsample.c" - #include "jpglib/jdtrans.c" - #include "jpglib/jfdctflt.c" - #include "jpglib/jfdctint.c" - #undef CONST_BITS - #undef MULTIPLY - #undef FIX_0_541196100 - #include "jpglib/jfdctfst.c" - #undef FIX_0_541196100 - #include "jpglib/jidctflt.c" - #undef CONST_BITS - #undef FIX_1_847759065 - #undef MULTIPLY - #undef DEQUANTIZE - #undef DESCALE - #include "jpglib/jidctfst.c" - #undef CONST_BITS - #undef FIX_1_847759065 - #undef MULTIPLY - #undef DEQUANTIZE - #include "jpglib/jidctint.c" - #include "jpglib/jidctred.c" - #include "jpglib/jmemmgr.c" - #include "jpglib/jmemnobs.c" - #include "jpglib/jquant1.c" - #include "jpglib/jquant2.c" - #include "jpglib/jutils.c" - #include "jpglib/transupp.c" - } -} - -#if JUCE_MSVC - #pragma warning (pop) -#endif - -BEGIN_JUCE_NAMESPACE - -#include "../juce_Image.h" -#include "../../../../../juce_core/io/juce_InputStream.h" -#include "../../../../../juce_core/io/juce_OutputStream.h" -#include "../../colour/juce_PixelFormats.h" - -using namespace jpeglibNamespace; - -//============================================================================== -struct JPEGDecodingFailure {}; - -static void fatalErrorHandler (j_common_ptr) -{ - throw JPEGDecodingFailure(); -} - -static void silentErrorCallback1 (j_common_ptr) {} -static void silentErrorCallback2 (j_common_ptr, int) {} -static void silentErrorCallback3 (j_common_ptr, char*) {} - -static void setupSilentErrorHandler (struct jpeg_error_mgr& err) -{ - zerostruct (err); - - err.error_exit = fatalErrorHandler; - err.emit_message = silentErrorCallback2; - err.output_message = silentErrorCallback1; - err.format_message = silentErrorCallback3; - err.reset_error_mgr = silentErrorCallback1; -} - - -//============================================================================== -static void dummyCallback1 (j_decompress_ptr) throw() -{ -} - -static void jpegSkip (j_decompress_ptr decompStruct, long num) throw() -{ - decompStruct->src->next_input_byte += num; - - num = jmin (num, (int) decompStruct->src->bytes_in_buffer); - decompStruct->src->bytes_in_buffer -= num; -} - -static boolean jpegFill (j_decompress_ptr) throw() -{ - return 0; -} - -//============================================================================== -Image* juce_loadJPEGImageFromStream (InputStream& in) throw() -{ - MemoryBlock mb; - in.readIntoMemoryBlock (mb); - - Image* image = 0; - - if (mb.getSize() > 16) - { - struct jpeg_decompress_struct jpegDecompStruct; - - struct jpeg_error_mgr jerr; - setupSilentErrorHandler (jerr); - jpegDecompStruct.err = &jerr; - - jpeg_create_decompress (&jpegDecompStruct); - - jpegDecompStruct.src = (jpeg_source_mgr*)(jpegDecompStruct.mem->alloc_small) - ((j_common_ptr)(&jpegDecompStruct), JPOOL_PERMANENT, sizeof (jpeg_source_mgr)); - - jpegDecompStruct.src->init_source = dummyCallback1; - jpegDecompStruct.src->fill_input_buffer = jpegFill; - jpegDecompStruct.src->skip_input_data = jpegSkip; - jpegDecompStruct.src->resync_to_restart = jpeg_resync_to_restart; - jpegDecompStruct.src->term_source = dummyCallback1; - - jpegDecompStruct.src->next_input_byte = (const unsigned char*) mb.getData(); - jpegDecompStruct.src->bytes_in_buffer = mb.getSize(); - - try - { - jpeg_read_header (&jpegDecompStruct, TRUE); - - jpeg_calc_output_dimensions (&jpegDecompStruct); - - const int width = jpegDecompStruct.output_width; - const int height = jpegDecompStruct.output_height; - - jpegDecompStruct.out_color_space = JCS_RGB; - - JSAMPARRAY buffer - = (*jpegDecompStruct.mem->alloc_sarray) ((j_common_ptr) &jpegDecompStruct, - JPOOL_IMAGE, - width * 3, 1); - - if (jpeg_start_decompress (&jpegDecompStruct)) - { - image = new Image (Image::RGB, width, height, false); - - for (int y = 0; y < height; ++y) - { - jpeg_read_scanlines (&jpegDecompStruct, buffer, 1); - - int stride, pixelStride; - uint8* pixels = image->lockPixelDataReadWrite (0, y, width, 1, stride, pixelStride); - const uint8* src = *buffer; - uint8* dest = pixels; - - for (int i = width; --i >= 0;) - { - ((PixelRGB*) dest)->setARGB (0, src[0], src[1], src[2]); - dest += pixelStride; - src += 3; - } - - image->releasePixelDataReadWrite (pixels); - } - - jpeg_finish_decompress (&jpegDecompStruct); - } - - jpeg_destroy_decompress (&jpegDecompStruct); - } - catch (...) - {} - } - - return image; -} - - -//============================================================================== -static const int bufferSize = 512; - -struct JuceJpegDest : public jpeg_destination_mgr -{ - OutputStream* output; - char* buffer; -}; - -static void jpegWriteInit (j_compress_ptr) throw() -{ -} - -static void jpegWriteTerminate (j_compress_ptr cinfo) throw() -{ - JuceJpegDest* const dest = (JuceJpegDest*) cinfo->dest; - - const int numToWrite = bufferSize - dest->free_in_buffer; - dest->output->write (dest->buffer, numToWrite); -} - -static boolean jpegWriteFlush (j_compress_ptr cinfo) throw() -{ - JuceJpegDest* const dest = (JuceJpegDest*) cinfo->dest; - - const int numToWrite = bufferSize; - - dest->next_output_byte = (JOCTET*) dest->buffer; - dest->free_in_buffer = bufferSize; - - return dest->output->write (dest->buffer, numToWrite); -} - -//============================================================================== -bool juce_writeJPEGImageToStream (const Image& image, - OutputStream& out, - float quality) throw() -{ - if (image.hasAlphaChannel()) - { - // this method could fill the background in white and still save the image.. - jassertfalse - return true; - } - - struct jpeg_compress_struct jpegCompStruct; - - struct jpeg_error_mgr jerr; - setupSilentErrorHandler (jerr); - jpegCompStruct.err = &jerr; - - jpeg_create_compress (&jpegCompStruct); - - JuceJpegDest dest; - jpegCompStruct.dest = &dest; - - dest.output = &out; - dest.buffer = (char*) juce_malloc (bufferSize); - dest.next_output_byte = (JOCTET*) dest.buffer; - dest.free_in_buffer = bufferSize; - dest.init_destination = jpegWriteInit; - dest.empty_output_buffer = jpegWriteFlush; - dest.term_destination = jpegWriteTerminate; - - jpegCompStruct.image_width = image.getWidth(); - jpegCompStruct.image_height = image.getHeight(); - jpegCompStruct.input_components = 3; - jpegCompStruct.in_color_space = JCS_RGB; - jpegCompStruct.write_JFIF_header = 1; - - jpegCompStruct.X_density = 72; - jpegCompStruct.Y_density = 72; - - jpeg_set_defaults (&jpegCompStruct); - - jpegCompStruct.dct_method = JDCT_FLOAT; - jpegCompStruct.optimize_coding = 1; -// jpegCompStruct.smoothing_factor = 10; - - if (quality < 0.0f) - quality = 0.85f; - - jpeg_set_quality (&jpegCompStruct, jlimit (0, 100, roundFloatToInt (quality * 100.0f)), TRUE); - - jpeg_start_compress (&jpegCompStruct, TRUE); - - const int strideBytes = jpegCompStruct.image_width * jpegCompStruct.input_components; - - JSAMPARRAY buffer = (*jpegCompStruct.mem->alloc_sarray) ((j_common_ptr) &jpegCompStruct, - JPOOL_IMAGE, - strideBytes, 1); - - while (jpegCompStruct.next_scanline < jpegCompStruct.image_height) - { - int stride, pixelStride; - const uint8* pixels = image.lockPixelDataReadOnly (0, jpegCompStruct.next_scanline, jpegCompStruct.image_width, 1, stride, pixelStride); - const uint8* src = pixels; - uint8* dst = *buffer; - - for (int i = jpegCompStruct.image_width; --i >= 0;) - { - *dst++ = ((const PixelRGB*) src)->getRed(); - *dst++ = ((const PixelRGB*) src)->getGreen(); - *dst++ = ((const PixelRGB*) src)->getBlue(); - src += pixelStride; - } - - jpeg_write_scanlines (&jpegCompStruct, buffer, 1); - image.releasePixelDataReadOnly (pixels); - } - - jpeg_finish_compress (&jpegCompStruct); - jpeg_destroy_compress (&jpegCompStruct); - - juce_free (dest.buffer); - - out.flush(); - - return true; -} - - -END_JUCE_NAMESPACE +/* + ============================================================================== + + This file is part of the JUCE library - "Jules' Utility Class Extensions" + Copyright 2004-7 by Raw Material Software ltd. + + ------------------------------------------------------------------------------ + + JUCE can be redistributed and/or modified 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. + + JUCE 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 JUCE; if not, visit www.gnu.org/licenses or write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA + + ------------------------------------------------------------------------------ + + If you'd like to release a closed-source product which uses JUCE, commercial + licenses are also available: visit www.rawmaterialsoftware.com/juce for + more information. + + ============================================================================== +*/ + +#include "../../../../../juce_core/basics/juce_StandardHeader.h" + +#if JUCE_MSVC + #pragma warning (push) +#endif + +namespace jpeglibNamespace +{ + extern "C" + { + #define JPEG_INTERNALS + #undef FAR + #include "jpglib/jpeglib.h" + + #include "jpglib/jcapimin.c" + #include "jpglib/jcapistd.c" + #include "jpglib/jccoefct.c" + #include "jpglib/jccolor.c" + #undef FIX + #include "jpglib/jcdctmgr.c" + #undef CONST_BITS + #include "jpglib/jchuff.c" + #undef emit_byte + #include "jpglib/jcinit.c" + #include "jpglib/jcmainct.c" + #include "jpglib/jcmarker.c" + #include "jpglib/jcmaster.c" + #include "jpglib/jcomapi.c" + #include "jpglib/jcparam.c" + #include "jpglib/jcphuff.c" + #include "jpglib/jcprepct.c" + #include "jpglib/jcsample.c" + #include "jpglib/jctrans.c" + #include "jpglib/jdapistd.c" + #include "jpglib/jdapimin.c" + #include "jpglib/jdatasrc.c" + #include "jpglib/jdcoefct.c" + #undef FIX + #include "jpglib/jdcolor.c" + #undef FIX + #include "jpglib/jddctmgr.c" + #undef CONST_BITS + #undef ASSIGN_STATE + #include "jpglib/jdhuff.c" + #include "jpglib/jdinput.c" + #include "jpglib/jdmainct.c" + #include "jpglib/jdmarker.c" + #include "jpglib/jdmaster.c" + #undef FIX + #include "jpglib/jdmerge.c" + #undef ASSIGN_STATE + #include "jpglib/jdphuff.c" + #include "jpglib/jdpostct.c" + #undef FIX + #include "jpglib/jdsample.c" + #include "jpglib/jdtrans.c" + #include "jpglib/jfdctflt.c" + #include "jpglib/jfdctint.c" + #undef CONST_BITS + #undef MULTIPLY + #undef FIX_0_541196100 + #include "jpglib/jfdctfst.c" + #undef FIX_0_541196100 + #include "jpglib/jidctflt.c" + #undef CONST_BITS + #undef FIX_1_847759065 + #undef MULTIPLY + #undef DEQUANTIZE + #undef DESCALE + #include "jpglib/jidctfst.c" + #undef CONST_BITS + #undef FIX_1_847759065 + #undef MULTIPLY + #undef DEQUANTIZE + #include "jpglib/jidctint.c" + #include "jpglib/jidctred.c" + #include "jpglib/jmemmgr.c" + #include "jpglib/jmemnobs.c" + #include "jpglib/jquant1.c" + #include "jpglib/jquant2.c" + #include "jpglib/jutils.c" + #include "jpglib/transupp.c" + } +} + +#if JUCE_MSVC + #pragma warning (pop) +#endif + +BEGIN_JUCE_NAMESPACE + +#include "../juce_Image.h" +#include "../../../../../juce_core/io/juce_InputStream.h" +#include "../../../../../juce_core/io/juce_OutputStream.h" +#include "../../colour/juce_PixelFormats.h" + +using namespace jpeglibNamespace; + +//============================================================================== +struct JPEGDecodingFailure {}; + +static void fatalErrorHandler (j_common_ptr) +{ + throw JPEGDecodingFailure(); +} + +static void silentErrorCallback1 (j_common_ptr) {} +static void silentErrorCallback2 (j_common_ptr, int) {} +static void silentErrorCallback3 (j_common_ptr, char*) {} + +static void setupSilentErrorHandler (struct jpeg_error_mgr& err) +{ + zerostruct (err); + + err.error_exit = fatalErrorHandler; + err.emit_message = silentErrorCallback2; + err.output_message = silentErrorCallback1; + err.format_message = silentErrorCallback3; + err.reset_error_mgr = silentErrorCallback1; +} + + +//============================================================================== +static void dummyCallback1 (j_decompress_ptr) throw() +{ +} + +static void jpegSkip (j_decompress_ptr decompStruct, long num) throw() +{ + decompStruct->src->next_input_byte += num; + + num = jmin (num, (int) decompStruct->src->bytes_in_buffer); + decompStruct->src->bytes_in_buffer -= num; +} + +static boolean jpegFill (j_decompress_ptr) throw() +{ + return 0; +} + +//============================================================================== +Image* juce_loadJPEGImageFromStream (InputStream& in) throw() +{ + MemoryBlock mb; + in.readIntoMemoryBlock (mb); + + Image* image = 0; + + if (mb.getSize() > 16) + { + struct jpeg_decompress_struct jpegDecompStruct; + + struct jpeg_error_mgr jerr; + setupSilentErrorHandler (jerr); + jpegDecompStruct.err = &jerr; + + jpeg_create_decompress (&jpegDecompStruct); + + jpegDecompStruct.src = (jpeg_source_mgr*)(jpegDecompStruct.mem->alloc_small) + ((j_common_ptr)(&jpegDecompStruct), JPOOL_PERMANENT, sizeof (jpeg_source_mgr)); + + jpegDecompStruct.src->init_source = dummyCallback1; + jpegDecompStruct.src->fill_input_buffer = jpegFill; + jpegDecompStruct.src->skip_input_data = jpegSkip; + jpegDecompStruct.src->resync_to_restart = jpeg_resync_to_restart; + jpegDecompStruct.src->term_source = dummyCallback1; + + jpegDecompStruct.src->next_input_byte = (const unsigned char*) mb.getData(); + jpegDecompStruct.src->bytes_in_buffer = mb.getSize(); + + try + { + jpeg_read_header (&jpegDecompStruct, TRUE); + + jpeg_calc_output_dimensions (&jpegDecompStruct); + + const int width = jpegDecompStruct.output_width; + const int height = jpegDecompStruct.output_height; + + jpegDecompStruct.out_color_space = JCS_RGB; + + JSAMPARRAY buffer + = (*jpegDecompStruct.mem->alloc_sarray) ((j_common_ptr) &jpegDecompStruct, + JPOOL_IMAGE, + width * 3, 1); + + if (jpeg_start_decompress (&jpegDecompStruct)) + { + image = new Image (Image::RGB, width, height, false); + + for (int y = 0; y < height; ++y) + { + jpeg_read_scanlines (&jpegDecompStruct, buffer, 1); + + int stride, pixelStride; + uint8* pixels = image->lockPixelDataReadWrite (0, y, width, 1, stride, pixelStride); + const uint8* src = *buffer; + uint8* dest = pixels; + + for (int i = width; --i >= 0;) + { + ((PixelRGB*) dest)->setARGB (0, src[0], src[1], src[2]); + dest += pixelStride; + src += 3; + } + + image->releasePixelDataReadWrite (pixels); + } + + jpeg_finish_decompress (&jpegDecompStruct); + } + + jpeg_destroy_decompress (&jpegDecompStruct); + } + catch (...) + {} + + in.setPosition (((char*) jpegDecompStruct.src->next_input_byte) - (char*) mb.getData()); + } + + return image; +} + + +//============================================================================== +static const int bufferSize = 512; + +struct JuceJpegDest : public jpeg_destination_mgr +{ + OutputStream* output; + char* buffer; +}; + +static void jpegWriteInit (j_compress_ptr) throw() +{ +} + +static void jpegWriteTerminate (j_compress_ptr cinfo) throw() +{ + JuceJpegDest* const dest = (JuceJpegDest*) cinfo->dest; + + const int numToWrite = bufferSize - dest->free_in_buffer; + dest->output->write (dest->buffer, numToWrite); +} + +static boolean jpegWriteFlush (j_compress_ptr cinfo) throw() +{ + JuceJpegDest* const dest = (JuceJpegDest*) cinfo->dest; + + const int numToWrite = bufferSize; + + dest->next_output_byte = (JOCTET*) dest->buffer; + dest->free_in_buffer = bufferSize; + + return dest->output->write (dest->buffer, numToWrite); +} + +//============================================================================== +bool juce_writeJPEGImageToStream (const Image& image, + OutputStream& out, + float quality) throw() +{ + if (image.hasAlphaChannel()) + { + // this method could fill the background in white and still save the image.. + jassertfalse + return true; + } + + struct jpeg_compress_struct jpegCompStruct; + + struct jpeg_error_mgr jerr; + setupSilentErrorHandler (jerr); + jpegCompStruct.err = &jerr; + + jpeg_create_compress (&jpegCompStruct); + + JuceJpegDest dest; + jpegCompStruct.dest = &dest; + + dest.output = &out; + dest.buffer = (char*) juce_malloc (bufferSize); + dest.next_output_byte = (JOCTET*) dest.buffer; + dest.free_in_buffer = bufferSize; + dest.init_destination = jpegWriteInit; + dest.empty_output_buffer = jpegWriteFlush; + dest.term_destination = jpegWriteTerminate; + + jpegCompStruct.image_width = image.getWidth(); + jpegCompStruct.image_height = image.getHeight(); + jpegCompStruct.input_components = 3; + jpegCompStruct.in_color_space = JCS_RGB; + jpegCompStruct.write_JFIF_header = 1; + + jpegCompStruct.X_density = 72; + jpegCompStruct.Y_density = 72; + + jpeg_set_defaults (&jpegCompStruct); + + jpegCompStruct.dct_method = JDCT_FLOAT; + jpegCompStruct.optimize_coding = 1; +// jpegCompStruct.smoothing_factor = 10; + + if (quality < 0.0f) + quality = 0.85f; + + jpeg_set_quality (&jpegCompStruct, jlimit (0, 100, roundFloatToInt (quality * 100.0f)), TRUE); + + jpeg_start_compress (&jpegCompStruct, TRUE); + + const int strideBytes = jpegCompStruct.image_width * jpegCompStruct.input_components; + + JSAMPARRAY buffer = (*jpegCompStruct.mem->alloc_sarray) ((j_common_ptr) &jpegCompStruct, + JPOOL_IMAGE, + strideBytes, 1); + + while (jpegCompStruct.next_scanline < jpegCompStruct.image_height) + { + int stride, pixelStride; + const uint8* pixels = image.lockPixelDataReadOnly (0, jpegCompStruct.next_scanline, jpegCompStruct.image_width, 1, stride, pixelStride); + const uint8* src = pixels; + uint8* dst = *buffer; + + for (int i = jpegCompStruct.image_width; --i >= 0;) + { + *dst++ = ((const PixelRGB*) src)->getRed(); + *dst++ = ((const PixelRGB*) src)->getGreen(); + *dst++ = ((const PixelRGB*) src)->getBlue(); + src += pixelStride; + } + + jpeg_write_scanlines (&jpegCompStruct, buffer, 1); + image.releasePixelDataReadOnly (pixels); + } + + jpeg_finish_compress (&jpegCompStruct); + jpeg_destroy_compress (&jpegCompStruct); + + juce_free (dest.buffer); + + out.flush(); + + return true; +} + + +END_JUCE_NAMESPACE diff --git a/src/juce_core/basics/juce_StandardHeader.h b/src/juce_core/basics/juce_StandardHeader.h index 59d7716c6a..b64ae7c9cc 100644 --- a/src/juce_core/basics/juce_StandardHeader.h +++ b/src/juce_core/basics/juce_StandardHeader.h @@ -81,6 +81,7 @@ #include #include #include +#include #if JUCE_MAC || JUCE_LINUX #include diff --git a/src/juce_core/containers/juce_BitArray.cpp b/src/juce_core/containers/juce_BitArray.cpp index a6abb507b0..1e6a85b6bd 100644 --- a/src/juce_core/containers/juce_BitArray.cpp +++ b/src/juce_core/containers/juce_BitArray.cpp @@ -958,7 +958,7 @@ void BitArray::parseString (const String& text, const MemoryBlock BitArray::toMemoryBlock() const throw() { - const int numBytes = (getHighestBit() + 7) >> 3; + const int numBytes = (getHighestBit() + 8) >> 3; MemoryBlock mb (numBytes); for (int i = 0; i < numBytes; ++i) diff --git a/src/juce_core/io/files/juce_File.cpp b/src/juce_core/io/files/juce_File.cpp index ae10027260..b28e609652 100644 --- a/src/juce_core/io/files/juce_File.cpp +++ b/src/juce_core/io/files/juce_File.cpp @@ -1068,14 +1068,8 @@ const String File::getRelativePathFrom (const File& dir) const throw() --commonBitLength; // if the only common bit is the root, then just return the full path.. -#if JUCE_WIN32 - if (commonBitLength <= 0 - || (commonBitLength == 1 && thisPath [1] == File::separator) - || (commonBitLength <= 3 && thisPath [1] == T(':'))) -#else if (commonBitLength <= 0 || (commonBitLength == 1 && thisPath [1] == File::separator)) -#endif return fullPath; thisPath = thisPath.substring (commonBitLength); diff --git a/src/juce_core/text/juce_String.cpp b/src/juce_core/text/juce_String.cpp index 47a8a66f5c..70ba0c73a7 100644 --- a/src/juce_core/text/juce_String.cpp +++ b/src/juce_core/text/juce_String.cpp @@ -1946,15 +1946,11 @@ const String String::toHexString (const unsigned char* data, *d++ = hexDigits [(*data) & 0xf]; ++data; - if (groupSize > 0 && (i % groupSize) == 0) + if (groupSize > 0 && (i % groupSize) == (groupSize - 1) && i < (size - 1)) *d++ = T(' '); } - if (groupSize > 0) - --d; - *d = 0; - return s; }