Wysiwyg html editor


#1

Hey guys,

I have made a very very very simple wysiwyg editor that now operates on HTML syntax, but can very easily be remade to support a wiki syntax and so on.
[attachment=1]Picture 4.jpg[/attachment]
[attachment=0]Picture 5.jpg[/attachment]

i guess its not the most efficient thing in the world, but it does its job.
except for one thing: when i change a font size (for instance i click on the h1 button), the line height of the entire text editor is changed, and not just the line of the enlarged text. i am guessing this is a bug with the text editor, unless i’m doing something wrong. would be glad to hear your inputs!

here is the .h file:

#ifndef RICHEDITOR_H
#define RICHEDITOR_H

class RichEditor  
: public Component, 
public ButtonListener,
public SliderListener,
public KeyListener,
public AsyncUpdater
{
	TextEditor * editor;
	
	StringArray startTokens;
	StringArray endTokens;
	Array <Font> fonts;
	Font font;

	bool isReadOnly;
	bool render;
	
	TextButton * btn_bold;
	TextButton * btn_under;
	TextButton * btn_italic;
	TextButton * btn_h1;
	TextButton * btn_h2;
	TextButton * btn_h3;
	Slider * fontSize;
	TextButton * btn_source;
	
public:
    
	RichEditor::RichEditor(bool IsReadOnly);
	RichEditor::~RichEditor();

    void RichEditor::resized();
	
	///////////////////////////////////
	// Controls
	///////////////////////////////////
	//this is just to make sure we render when people copy or type
	bool RichEditor::keyPressed (const KeyPress& key, Component* originatingComponent);
	void RichEditor::handleAsyncUpdate();
	
	bool RichEditor::keyPressed (const KeyPress& key);
	void RichEditor::buttonClicked (Button* button);
	void RichEditor::sliderValueChanged (Slider* slider);
	void RichEditor::setFont(Font newFont);
	
	///////////////////////////////////
	// Rendering
	///////////////////////////////////	
	void RichEditor::defineTokens();
	void RichEditor::applyFormating(int formatType);
	void RichEditor::renderText();
	
	///////////////////////////////////
	// Data Exchange
	///////////////////////////////////
	TextEditor * RichEditor::getEditor();
	// this adds "<br>\n" instead of \n or alternatively just "<br>" to save space while compromising readability.
	String RichEditor::getHTML(bool newLineOnBR=true);
	// this is for transfering between rich editors without the <br>
	String RichEditor::getText();
	void RichEditor::setText(String text);
	String RichEditor::getCleanText();
	juce_UseDebuggingNewOperator
};

#endif

here is the .cpp file:

#include "juce.h"
#include "richEditor.h"

RichEditor::RichEditor (bool IsReadOnly)
:font(14.f),
isReadOnly(IsReadOnly),
render(true)
{

	if (!IsReadOnly)
	{
		addAndMakeVisible(btn_bold = new TextButton("B","Bold Ctrl+b"));
		btn_bold->setBounds(5,0,30,25);	
		btn_bold->addButtonListener(this);
		
		addAndMakeVisible(btn_under = new TextButton("u", "Underline Ctrl+u"));
		btn_under->setBounds(35,0,30,25);
		btn_under->addButtonListener(this);
		
		addAndMakeVisible(btn_italic = new TextButton("i","Bold Ctrl+i"));
		btn_italic->setBounds(65,0,30,25);
		btn_italic->addButtonListener(this);
		
		addAndMakeVisible(btn_h1 = new TextButton("h1", "Headline Level 1 Ctrl+1"));
		btn_h1->setBounds(95,0,30,25);
		btn_h1->addButtonListener(this);
		
		addAndMakeVisible(btn_h2 = new TextButton("h2","Headline Level 2 Ctrl+2"));
		btn_h2->setBounds(125,0,30,25);
		btn_h2->addButtonListener(this);
		
		addAndMakeVisible(btn_h3 = new TextButton("h3", "Headline Level 3 Ctrl+3"));
		btn_h3->setBounds(155,0,30,25);
		btn_h3->addButtonListener(this);
		
		addAndMakeVisible(btn_source = new TextButton("HTML", "Show HTML source Ctrl+h"));
		btn_source->setBounds(185,0,60,25);
		btn_source->addButtonListener(this);
		btn_source->setClickingTogglesState(true);
		
		addAndMakeVisible(fontSize = new Slider("Font size"));
		fontSize->setRange(8., 48., .5);
		fontSize->setBounds(250,5,100,15);
		fontSize->addListener(this);
		fontSize->setValue(14., false, false);
		fontSize->setTextBoxStyle(Slider::TextBoxLeft, false, 30, 15);
	}
	addAndMakeVisible(editor = new TextEditor());
	editor->setMultiLine(true, true);
	editor->setReturnKeyStartsNewLine(true);
	editor->setTabKeyUsedAsCharacter(true);
	if (IsReadOnly)
	{
		editor->setReadOnly(true);
		editor->setPopupMenuEnabled(false);
	}
	else
		editor->addKeyListener(this);

	defineTokens();
}

RichEditor::~RichEditor()
{
	deleteAllChildren();
}

void RichEditor::resized()
{
	if (isReadOnly)
		editor->setBounds(0,0,getWidth(),getHeight());
	else
		editor->setBounds(0,30,getWidth(),getHeight()-30);
			
}

///////////////////////////////////
// Controls
///////////////////////////////////

bool RichEditor::keyPressed (const KeyPress& key, Component* originatingComponent)
{
	triggerAsyncUpdate();
	return false;
}

void RichEditor::handleAsyncUpdate()
{
	renderText();
}

bool RichEditor::keyPressed (const KeyPress& key)
{
	if (key == KeyPress (T('b'), ModifierKeys::commandModifier, 0))
	{
		applyFormating(1);
		return true;
	}
	else if (key == KeyPress (T('u'), ModifierKeys::commandModifier, 0))
	{
		applyFormating(2);
		return true;
	}
	else if (key == KeyPress (T('i'), ModifierKeys::commandModifier, 0))
	{
		applyFormating(3);
		return true;
	}
	else if (key == KeyPress (T('1'), ModifierKeys::commandModifier, 0))
	{
		applyFormating(4);
		return true;
	}
	else if (key == KeyPress (T('2'), ModifierKeys::commandModifier, 0))
	{
		applyFormating(5);
		return true;
	}
	else if (key == KeyPress (T('3'), ModifierKeys::commandModifier, 0))
	{
		applyFormating(6);
		return true;
	}
	
	else if (key == KeyPress (T('h'), ModifierKeys::commandModifier, 0))
	{
		applyFormating(-1);
		return true;
	}
	
	return false;
}


void RichEditor::buttonClicked (Button* button)
{
	if (button == btn_bold)
		applyFormating(1);
	else if (button == btn_under)
		applyFormating(2);
	else if (button == btn_italic)
		applyFormating(3);
	else if (button == btn_h1)
		applyFormating(4);
	else if (button == btn_h2)
		applyFormating(5);
	else if (button == btn_h3)
		applyFormating(6);
	else if (button == btn_source)
	{
		render = !btn_source->getToggleState();
		applyFormating(-1);
	}
	
	editor->grabKeyboardFocus();
}

void RichEditor::sliderValueChanged (Slider* slider)
{
	setFont(slider->getValue());
}

void RichEditor::setFont(Font newFont)
{
	font = newFont;
	defineTokens();
	renderText();
}


///////////////////////////////////
// Rendering
///////////////////////////////////

void RichEditor::defineTokens()
{
	startTokens.clear();
	endTokens.clear();
	fonts.clear();
	
	startTokens.add("");
	endTokens.add("");
	fonts.add(font);
	
	startTokens.add("<b>");
	endTokens.add("</b>");
	font.setBold(true);
	fonts.add(font);
	font.setBold(false);
	
	startTokens.add("<u>");
	endTokens.add("</u>");
	font.setUnderline(true);
	fonts.add(font);
	font.setUnderline(false);
	
	startTokens.add("<i>");
	endTokens.add("</i>");
	font.setItalic(true);
	fonts.add(font);
	font.setItalic(false);
	
	startTokens.add("<h1>");
	endTokens.add("</h1>");
	float height = font.getHeight();
	font.setHeight(height*2.f);
	font.setBold(true);
	fonts.add(font);
	font.setBold(false);
	font.setHeight(height);
	
	startTokens.add("<h2>");
	endTokens.add("</h2>");
	font.setHeight(height*1.5f);
	font.setBold(true);
	fonts.add(font);
	font.setBold(false);
	font.setHeight(height);
	
	startTokens.add("<h3>");
	endTokens.add("</h3>");
	font.setHeight(height*1.25f);
	font.setBold(true);
	fonts.add(font);
	font.setBold(false);
	font.setHeight(height);
	
}

void RichEditor::applyFormating(int formatType)
{
	if (formatType > 0)
	{
		String text = editor->getHighlightedText();
		
		if (text.length()>0)
			editor->insertTextAtCursor(startTokens[formatType] + text + endTokens[formatType]);
	}
	renderText();
}

void RichEditor::renderText()
{
	String text = editor->getText();
	editor->applyFontToAllText(fonts[0]);
	
	if (render)
	{

		String replace, startToken, endToken;
		int index, indexEnd, startLength,endLength;
		int carentPos = editor->getCaretPosition();
		int size = startTokens.size();
		for (int i = 1; i<size;i++)
		{
			startToken = startTokens[i];
			endToken = endTokens[i];
			index = text.indexOf(startToken);
			indexEnd = text.indexOf(endToken);
			
			startLength = startToken.length();
			endLength = endToken.length();
			
			while(index!=-1 && indexEnd!=-1 && index<indexEnd)
			{		
				editor->setHighlightedRegion(index, indexEnd-index+endLength);
				replace = editor->getHighlightedText().substring(startLength).dropLastCharacters(endLength);
				editor->setFont(0);
				editor->insertTextAtCursor(startToken);
				editor->setFont(fonts[i]);
				editor->insertTextAtCursor(replace);
				editor->setFont(0);
				editor->insertTextAtCursor(endToken);
				
				text = editor->getText();
				index = text.indexOf(indexEnd,startToken);
				indexEnd = text.indexOf(indexEnd+1,endToken);
			}
		}
		editor->setFont(fonts[0]);
		editor->setCaretPosition(carentPos);
	}
	editor->resized();
	//DBG(getHTML());
}


///////////////////////////////////
// Data Exchange
///////////////////////////////////

TextEditor * RichEditor::getEditor()
{
	return editor;
}

String RichEditor::getHTML(bool newLineOnBR)
{
	return editor->getText().replace(T("\n"),newLineOnBR ? T( "<br>\n"):T("<br>"), false);
}

String RichEditor::getText()
{
	return editor->getText();
}

void RichEditor::setText(String text)
{
	editor->setText(text,false);
	renderText();
}

String RichEditor::getCleanText()
{
	int size = startTokens.size();
	String text = editor->getText();
	for (int i = 1; i<size;i++)
	{
		text = text.replace(startTokens[i], String::empty, true);
		text = text.replace(endTokens[i], String::empty, true);
	}
	return text;
}

enjoy!


#2

That’s pretty cool - no, it won’t be the most efficient editor in the world, but it’s a neat thing to get working in so little code!


#3

wow from the master himself :oops:

do you have any idea why bigger fonts set the line height to all of the following lines as well?


#4

hmm. Sounds like a bugette. I’ll take a look…


#5

Ok, found a bug and have checked-in a fix now if you want to try it. Thanks!


#6

ur the best!
:smiley:
it works, and now i can type with caps without assertion calls :smiley:


#7

ill optimize it a bit and repost, i can save several “render” passes, and make the rendering more efficient…

is there any chance for text alignment features? :slight_smile:


#8

I’ve got some code sitting around that was sent to me that does alignment - just haven’t had time to go through and merge it yet.


#9

that could be awsome.

i know this is a bit pesky but… ive noticed another small issue, the Tab char (\t) renders as only one space. it could be very nice to have it act as a real tab key (making space until the next grid).


#10

(I think it actually inserts a tab character, not a space.)

I avoided the issue of tabs in the editor because when you have tabs, you then have to have a way of specifying whether the tab key changes focus or not, and setting the tab size, and then people will want variable spacing etc etc… and next thing you know, it’s become Microsoft Word! The editor’s not really designed for that kind of heavy duty use!


#11

yes, it inserts a tab chatecter \t
is it possible to just make it render as 4-5 spaces instead of 1? maybe its a mac thing?

im not trying to make a word thing, i’m trying to get my software to be able to show decent looking help files.
and on the way, i can also upload them to my site or easily convert to wiki if i like.

this editor just supports the very basic html tags.


  • but its enough to be used with a css wrapper for a site :slight_smile:
  • i’m using the /t along with the

    and

  • for an indenting effect.

  • #12

    Small notification to state that this doesn’t work well anymore with the last tip (yes, I’ve fixed the method name change, beware of “editor->setHighlightedRegion(Range(index, indexEnd + endLength))”)
    This is likely due to the UndoManager now in TextEditor, that can’t be ignored.

    So, a welcome fix would be find & replace in TextEditor.cpp:
    &undoManager
    by
    isReadOnly() ? 0 : &undoManager

    (Or add a new “useUndoManager” flag in such class)


    #13