HOWTO

Include Ferry in your project

Ferry project consists of two top-level libraries: Ferry Core library and Ferry Extensions library. For simplicity these two libraries are referred with "Ferry library" term. Each library can be compiled as static or shared. Additionally they can be packed together in a single static or shared library. The exact configuration is defined with build flags (see Build Notes for more info).

fe/ferry.h is the top level header for Ferry Core library.

fe/ferry_extensions.h is the top level header for Ferry Extensions library.

Declaration of Fe_CreateEditor() function that creates Ferry editors is accessible through fe/ferry_create_editor.h header.

Add one or more of #include "fe/ferry.h", #include "fe/ferry_create_editor.h" and #include "fe/ferry_extensions.h" preprocessor directives to your source files depending on what functionality you need.

On Windows you also need to specify import library with exported Ferry symbols on the linker's command line to successfully build your software. As different compilers have different import library formats you need to create Ferry import library/libraries on you own using fe.def and/or feext.def files for input.

Create editor

#include "fe/ferry.h"
#include "fe/ferry_create_editor.h"


namespace
{

WNDPROC feWndProc = 0;


LRESULT CALLBACK
wndProcA(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if(uMsg == WM_DESTROY)
    {
        // make GetMessage() return 0 and thus exit event loop normally
        PostQuitMessage(0);
    }

    return CallWindowProcA(feWndProc, hWnd, uMsg, wParam, lParam);
}


LRESULT CALLBACK
wndProcW(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if(uMsg == WM_DESTROY)
    {
        // make GetMessage() return 0 and thus exit event loop normally
        PostQuitMessage(0);
    }

    return CallWindowProcW(feWndProc, hWnd, uMsg, wParam, lParam);
}

} // namespace


bool
createFerryEditorAndBlock()
{
    fe::CreateEditorArgs args;

    // Create Ferry editor as a top level window with both vertical and
    // horizontal scrollbars enabled.
    // You can create Ferry editor as a child window as well.
    args.dwExStyle  = 0;
    args.dwStyle    =
        WS_OVERLAPPEDWINDOW
        | WS_VISIBLE
        | WS_VSCROLL
        | WS_HSCROLL;
    args.hWndParent = NULL;

    // Initial position and size.
    args.x          = 0;
    args.y          = 0;
    args.nWidth     = 200;
    args.nHeight    = 200;

    // No menu
    args.hMenu      = NULL;

    // Some essentials
    args.hInstance  = GetModuleHandle(NULL);
    args.lpParam    = NULL;

    // Try to create Unicode window at first.
    args.unicode    = true;

    // Fe_CreateEditor() will call RegisterClass() to register
    // Ferry window class implicitly if needed. UnregisterClass() is called
    // after the last created Ferry editor window is destroyed.
    fe::PlatformTypes::wnd_handle_type hwnd = Fe_CreateEditor(args);
    if(!hwnd)
    {   // Failed to create Ferry editor window.
        // One of reasons is that we try to create Unicode window running
        // under an OS that doesn't support Unicode (Windows 98).
        // So try to create ANSI window. This doesn't affect Unicode
        // capabilities of an editor itself, just instructs Windows that it
        // should create window that will receive ANSI messages.
        args.unicode = false;

        hwnd = Fe_CreateEditor(args);
        if(!hwnd)
        {   // Something went wrong.
            return false;
        }
    }

    // Get the default Ferry window procedure.
    feWndProc = (WNDPROC)GetWindowLong(hwnd, GWL_WNDPROC);
    if(!feWndProc)
    {   // Something went wrong.
        return false;
    }

    // Install custom window procedure that will call PostQuitMessage()
    // function to abort event loop. Install the proper custom window
    // procedure depending on Ferry window type (ANSI/Unicode).
    if(IsWindowUnicode(hwnd))
    {
        SetWindowLongW(hwnd, GWL_WNDPROC, (LONG)wndProcW);
    }
    else
    {
        SetWindowLongA(hwnd, GWL_WNDPROC, (LONG)wndProcA);
    }

    // Set window title.
    SetWindowTextA(hwnd, "Ferry");

    // Make just created window foreground.
    SetForegroundWindow(hwnd);

    // Run event loop.
    for(MSG msg; GetMessage(&msg, NULL, 0, 0) > 0; )
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return true;
}

Open file in editor

#include <fstream>
#include <vector>

#include "fe/ferry.h"


void
openFile(fe::PlatformTypes::wnd_handle_type hwnd, const char* fname)
{
    // hwnd - platform specific handle addressing Ferry editor window;
    // fname - name of file with UTF8-encoded data to open;

    // The function can throw exceptions of the following types:
    //  - std::exception
    //  - fe::FeError

    std::vector<fe::byte_type> buf;

    {
        std::ifstream file;

        // Throw std::exception-based exceptions from every failed method called
        // on "file".
        file.exceptions(
            std::ios_base::badbit
            | std::ios_base::eofbit
            | std::ios_base::failbit);

        // Open file in binary mode for reading. Binary mode is to preserve
        // EOL markers.
        file.open(fname, std::ios_base::binary | std::ios_base::in);

        // Get file length.
        file.seekg(0, std::ios_base::end);
        long fileLen = file.tellg();

        // Rewind file pointer.
        file.seekg(0, std::ios_base::beg);

        // Reserve buffer enough to hold file contents.
        buf.resize(fileLen);

        // Load file in the buffer.
        file.read(&*buf.begin(), buf.size());
    }

    // Construct string source that will tokenize file contents into
    // separate strings at EOL markers.
    std::auto_ptr<fe::IStringSource> ss(
        Fe_TokenizeUtf8String(
            &*buf.begin(), buf.size(), false /* Keep EOL markers */));
    if(0 == ss.get())
    {
        throw std::runtime_error("failed to open file");
    }

    // Construct transient peer to manage editor.
    fe::EditorFacadePeer editor(hwnd);

    // Clear any existing text and undo/redo stack.
    // Note: can throw fe::FeError!
    editor.clearText();

    // Load new text. "fe::eolVoid" indicates that editor shouldn't append
    // EOL markers to strings that "ss" string source produces as they
    // already have EOL markers.
    // Note: can throw fe::FeError!
    editor.insertText(0, ss.get(), fe::eolVoid);

    // Delete a single edit action created with the above insertText() call
    // from the undo/redo stack. If not done, user will be able to revert
    // text insertion which is undesirable when text is just loaded from a
    // file.
    // Note: can throw fe::FeError!
    editor.clearActions();
}

Get text from editor

The simplest solution:
#include <string>
#include <memory>

#include "fe/ferry.h"


std::string
getTextUTF8(fe::PlatformTypes::wnd_handle_type hwnd)
{
    // hwnd - platform specific handle addressing Ferry editor window;

    // The function can throw exceptions of the following types:
    //  - std::exception
    //  - fe::FeError

    // Construct transient peer to access text in editor.
    fe::EditorFacadePeer editor(hwnd);

    // Get pointer to heap allocated object to iterate over editor's text.
    // Note: can throw fe::FeError!
    std::auto_ptr<fe::ITextIterator> it(editor.createTextIterator());

    std::string buf;

    std::string eolMarker;

    // If text lines in editor already have EOL markers, then no additional
    // EOL markers should be appended to the 'buf'. Otherwise init 'eolMarker'
    // with '\r' or '\r\n' or some other EOL byte sequence of your taste.
    //
    // Note: options to configure EOL markers in text lines:
    //  - fe::EditorFacadePolicy::eolClipboardPaste
    //  - fe::EditorFacadePolicy::eolSplitLine
    //  - Fe_TokenizeUtf8String()
    //

    // Rewind iterator and iterate over text lines up to the last one.
    // Note: can throw fe::FeError!
    fe::String textLine = it->gotoLine(0);
    while(textLine.ptr)
    {
        buf.append(
            static_cast<const std::string::value_type*>(textLine.ptr),
            textLine.len);
        if(eolMarker.size())
        {
            buf.append(eolMarker);
        }

        // Note: can throw fe::FeError!
        textLine = it->nextLine();
    }

    return buf;
}

The getTextUTF8() function works as expected in single-threaded application. In multi-threaded application, when the function is called in a thread other then the thread running event loop of an editor, direct call to getTextUTF8() may produce unexpected result as text can be changed asynchronously by user. Two solutions can be proposed.

The first one is to call getTextUTF8() function in the context of the thread running event loop of an editor using fe::Fe_ExecTask() function. This guarantees that no text change can happen in the middle of iteration over text lines in editor:

std::string
getTextUTF8Sync(fe::PlatformTypes::wnd_handle_type hwnd)
{
    class Task : public fe::IEditorFacadeTask
    {
    UC_DLL_INTERFACE_IMPL(IEditorFacadeTask)
    private:
        std::string buf_;
        bool        error_;

    public:
        Task(): error_(false)
        {
        }

        const std::string&
        getText() const
        {
            if(error_)
            {   // TBD: throw exception to indicate that exception occurred in
                // exec() function
            }
            return buf_;
        }

    // fe::IEditorFacadeTask implementation
    public:
        virtual void UC_DLL_CALL
        exec(fe::FeHandle* handle)
        {
            try
            {
                buf_ = getTextUTF8(::Fe_GetPeer(handle));
                return;
            }
            catch(const fe::FeError&)
            {   // TBD: report exception
            }
            catch(const std::exception&)
            {   // TBD: report exception
            }
            catch(...)
            {   // TBD: report unexpected exception
            }
            error_ = true;
        }
    } task;

    fe::EditorFacadePeer editor(hwnd);

    // Will throw fe::FeError if fails to call Task::exec()
    editor.execTask(task);

    return task.getText();
}

The disadvantage of this approach is that user will not be able to interact with the editor as the thread running its event loop will be blocked until getTextUTF8Sync() completes.

The second solution is to disable interactive text modification in the editor before calling getTextUTF8() function. It may be convenient to use fe::ReadOnlyTrigger helper class to temporary disable interactive text modification.

See also Simple find & replace for example of fe::ReadOnlyTrigger use.

Test if text in editor changed

Call fe::IEditorFacade::canUndo() and check if the result is >0 to determine if text has been changed since the last fe::IEditorFacade::clearActions() or fe::IEditorFacade::clearText() call if any.

If you need to test if text in editor changed since the last time you saved the text in a file do the following:

To determine if text in editor is not synchronized with the corresponding file compare current value of undoActionsSinceSave counter and result of fe::IEditorFacade::canUndo() call: if they are not equal synchronization is needed.

Enable display of whitespace, TAB and control characters

#include "fe/ferry.h"
#include "fe/ferry_extensions.h"

#include <memory> // std::auto_ptr


class CtrlCharsError
{
public:
    CtrlCharsError()
    {
    }

    explicit CtrlCharsError(int reason)
    {   // ignore 'reason' value for simplicity
    }
};


fe::IExtension*
createDisplayCtrlCharsEditorExtension()
{
    fe::IExtension* ext = 0;

    // Create factory.
    std::auto_ptr<fe::ICharacterDecoratorFactory> factory(
        Fe_CreateCharacterDecoratorFactory());

    if(factory.get())
    {
        typedef fe::CharClassConfigurator<CtrlCharsError> ConfiguratorType;

        // Add character class that encapsulates all control characters
        // to the factory.
        // Note: can throw CtrlCharsError instance on error.
        ConfiguratorType ctrlCharsClass(
            *factory, fe::ICharacterDecoratorFactory::cctControl);
        ctrlCharsClass.commit(
            fe::ICharacterDecoratorFactory::ccdCharCodeUtf32);

        // Add character class that encapsulates a single TAB character
        // to the factory.
        // Note: can throw CtrlCharsError instance on error.
        ConfiguratorType tabCharClass(
            *factory, fe::ICharacterDecoratorFactory::cctTab);
        tabCharClass.commit(fe::ICharacterDecoratorFactory::ccdArrow);

        // Add character class that encapsulates whitespace characters
        // to the factory.
        // Note: can throw CtrlCharsError instance on error.
        ConfiguratorType spaceCharClass(
            *factory, fe::ICharacterDecoratorFactory::cctWhiteSpace);
        spaceCharClass.commit(fe::ICharacterDecoratorFactory::ccdDot);

        // Create editor extension.
        ext = factory->createExtension();
    }

    return ext;
}

The simplest use of createDisplayCtrlCharsEditorExtension() function:

namespace
{
    std::auto_ptr<fe::IExtension>   ctrCharsExtension;
    fe::FeHandle*                   editor;
}

void
displayCtrlChars()
{
    std::auto_ptr<fe::IExtension> ext(
        createDisplayCtrlCharsEditorExtension());

    if(!ext.get() || !ext->attachToEditor(editor))
    {
        throw CtrlCharsError();
    }

    ctrCharsExtension = ext;
}

void
undisplayCtrlChars()
{
    ctrCharsExtension = std::auto_ptr<fe::IExtension>(0);
}

The below images show how text data loaded in the editor is displayed before and after displayCtrlChars() function is called.

Default decorator, before displayCtrlChars() call:

ctrl_chars0.png

Default decorator, after displayCtrlChars() call:

ctrl_chars1.png

Scintilla decorator, before displayCtrlChars() call:

ctrl_chars2.png

Scintilla decorator, after displayCtrlChars() call:

ctrl_chars3.png

ICharacterDecoratorFactory configuration - another example of fe::ICharacterDecoratorFactory class usage.

Simple find & replace

#include <string>
#include <algorithm>

#include "fe/ferry.h"


void
replaceSubstring(
    fe::PlatformTypes::wnd_handle_type  hwnd,
    const std::string&                  from,
    const std::string&                  to)
{
    // hwnd - platform specific handle addressing Ferry editor window;
    // from - UTF8-encoded substring to replace;
    // to   - UTF8-encoded replacement string;

    // The function can throw exceptions of the following types:
    //  - std::exception
    //  - fe::FeError

    if(!from.size())
    {
        return;
    }

    // Construct transient peer to access text in editor.
    fe::EditorFacadePeer editor(hwnd);

    // Disable interactive text change in the editor until exit from the
    // function.
    fe::ReadOnlyTrigger setReadonly(editor.getHandle());

    // Get pointer to heap allocated object to iterate over editor's text.
    // Note: can throw fe::FeError!
    std::auto_ptr<fe::ITextIterator> it(editor.createTextIterator());

    fe::String toStr;
    toStr.ptr = to.data();
    toStr.len = to.size();

    const int diff = to.size() - from.size();

    // Group all replacements in a single undo action.
    // This will allow user to revert text change (if needed)
    // with a single "Ctrl+Z".
    fe::ActionGroupTrigger groupActions(
        editor.getHandle(), false /* Don't disable screen update */);

    // Rewind iterator and iterate over text lines up to the last one.
    // Note: can throw fe::FeError!
    fe::String textLine = it->gotoLine(0);
    while(textLine.ptr)
    {
        std::string::size_type  pos                 = 0;
        unsigned                curLineFirstByteIdx = (unsigned)-1;
        unsigned                curDiff             = 0;
        for(;;)
        {
            unsigned cur =
                std::search(
                    textLine.ptr + pos,
                    textLine.ptr + textLine.len,
                    from.begin(), from.end()) - textLine.ptr;
            if(textLine.len == cur)
            {
                break;
            }

            if(curLineFirstByteIdx == (unsigned)-1)
            {   // Initialize "curLineFirstByteIdx" only once to avoid
                // multiple "it->getCurLineFirstByteIdx()" calls for the same
                // text line.
                curLineFirstByteIdx = it->getCurLineFirstByteIdx();
            }

            cur += curLineFirstByteIdx;
            pos = cur + from.size();

            {
                // This group is need to minimize screen repaints and to make
                // substring replacement smooth.
                fe::ActionGroupTrigger atom(
                    editor.getHandle(), true /* Disable screen update */);

                // Select substring to replace in the editor.
                // Note: can throw fe::FeError!
                editor.moveCursorToChar(
                    pos + curDiff, false /* clear selection */);
                // Note: can throw fe::FeError!
                editor.moveCursorToChar(
                    cur + curDiff, true /* expand selection */);

                // Delete substring to replace.
                // Note: can throw fe::FeError!
                editor.del();

                // Insert the replacement string.
                fe::SingleItemStringSource ss(toStr);
                // Note: can throw fe::FeError!
                editor.insertText(cur + curDiff, &ss, fe::eolVoid);
            }

#ifdef _WIN32
            // Sleep a little after replacement to let user see visual effect
            // before moving to the next replacement.
            Sleep(500);
#endif

            curDiff += diff;
            pos     -= curLineFirstByteIdx;
        }

        // Note: can throw fe::FeError!
        textLine = it->nextLine();
    }
}

replaceSubstring() replaces all instances of from substring with to. The function is designed to run asynchronously, i.e. user can still interact with the editor (perform any r/o operation) while this function runs. Multi-line match is not supported.


Generated on Tue Nov 18 21:08:22 2008 for Ferry by doxygen 1.5.7.1
http://sourceforge.net