Monday, September 2, 2013

[Qt] Custom QListView delegate with word wrap

Using QListView with a custom item layout has been a very popular demand by many developers. The problem with using  a custom delegate was having some basic features like word wrap depending on the allowed list size.

Here is a screenshot from a project prototype I've been working on. The text will wrap around to fit in multiple lines depending on text length and list view width.


My custom delegate is about using two important methods: QFontMetrics::boundingRect() and QPainter::drawText() .

The first method will return the minimum Rect needed to draw the given text using the given origin and font. The second method draws the given text within the given Rect. Note that the flag Qt::TextWordWrap is given to both methods when called.

The code is simple:
  • Get system default font.
  • Process the font to suite your needs. In my case: I have a normal font and another bold one for the header.
  • Create a QFontMetrics instance for each font.
  • Use the QFontMetrics instances to calculate the Rect needed to draw that text given the needed flags and option.rect.width() that indicates the allowed width inside the list view.
  • Calculate the total area needed to return it in the sizeHint() method of your list view delegate. For example: I return the total heights of the two calculated rects -header and subheader- and the item width from option.rect.width(). And I prefer to add some extra padding.
  • Almost the same calculations apply in the paint() method of the list view delegate. Then the calculations are used in drawText().
Here is my ListVewDelegate class. It is a basic class with a basic drawing for header and subheader text. I left a space for drawing an icon. I also made a simple highlighting that needs more improvement and styling.

======
[listviewdelegate.h]
#ifndef LISTVIEWDELEGATE_H
#define LISTVIEWDELEGATE_H

#include <QStyledItemDelegate>
#include <QLabel>
#include <QPainter>
#include <QApplication>

class ListViewDelegate : public QStyledItemDelegate
{
public:
    enum datarole { HeaderRole = Qt::UserRole + 100, SubheaderRole};

    ListViewDelegate();
    ~ListViewDelegate();

    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const;

    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index ) const;

    static QSize iconSize;
    static int padding;
};

#endif // LISTVIEWDELEGATE_H

======
[listviewdelegate.cpp]
#include "listviewdelegate.h"

QSize ListViewDelegate::iconSize = QSize(60, 60);
int ListViewDelegate::padding = 5;

ListViewDelegate::ListViewDelegate()
{
}

ListViewDelegate::~ListViewDelegate()
{
}

QSize ListViewDelegate::sizeHint(const QStyleOptionViewItem &  option ,
                                        const QModelIndex & index) const
{
    if(!index.isValid())
        return QSize();

    QString headerText = index.data(HeaderRole).toString();
    QString subheaderText = index.data(SubheaderRole).toString();

    QFont headerFont = QApplication::font();
    headerFont.setBold(true);
    QFont subheaderFont = QApplication::font();
    QFontMetrics headerFm(headerFont);
    QFontMetrics subheaderFm(subheaderFont);

    /* No need for x,y here. we only need to calculate the height given the width.
     * Note that the given height is 0. That is because boundingRect() will return
     * the suitable height if the given geometry does not fit. And this is exactly
     * what we want.
     */
    QRect headerRect = headerFm.boundingRect(0, 0,
                                             option.rect.width() - iconSize.width(), 0,
                                             Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap,
                                             headerText);
    QRect subheaderRect = subheaderFm.boundingRect(0, 0,
                                                   option.rect.width() - iconSize.width(), 0,
                                                   Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap,
                                                   subheaderText);

    QSize size(option.rect.width(), headerRect.height() + subheaderRect.height() +  3*padding);

    /* Keep the minimum height needed in mind. */
    if(size.height()<iconSize.height())
        size.setHeight(iconSize.height());

    return size;
}

void ListViewDelegate::paint(QPainter *painter,
                                    const QStyleOptionViewItem &option,
                                    const QModelIndex &index) const
{
    if(!index.isValid())
        return;

    painter->save();

    if (option.state & QStyle::State_Selected)
        painter->fillRect(option.rect, option.palette.highlight());

    QString headerText = index.data(HeaderRole).toString();
    QString subheaderText = index.data(SubheaderRole).toString();

    QFont headerFont = QApplication::font();
    headerFont.setBold(true);
    QFont subheaderFont = QApplication::font();
    QFontMetrics headerFm(headerFont);
    QFontMetrics subheaderFm(subheaderFont);

    /*
     * The x,y coords are not (0,0) but values given by 'option'. So, calculate the
     * rects again given the x,y,w.
     * Note that the given height is 0. That is because boundingRect() will return
     * the suitable height if the given geometry does not fit. And this is exactly
     * what we want.
     */
    QRect headerRect =
            headerFm.boundingRect(option.rect.left() + iconSize.width(), option.rect.top() + padding,
                                  option.rect.width() - iconSize.width(), 0,
                                  Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap,
                                  headerText);
    QRect subheaderRect =
            subheaderFm.boundingRect(headerRect.left(), headerRect.bottom()+padding,
                                     option.rect.width() - iconSize.width(), 0,
                                     Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap,
                                     subheaderText);

    painter->setPen(Qt::black);

    painter->setFont(headerFont);
    painter->drawText(headerRect, Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap, headerText);

    painter->setFont(subheaderFont);
    painter->drawText(subheaderRect, Qt::AlignLeft|Qt::AlignTop|Qt::TextWordWrap, subheaderText);

    painter->restore();
}

===========

Using it is pretty simple:

[mainwindow.cpp]
QStandardItemModel *model;  
model = new QStandardItemModel();  

ListViewDelegate *listdelegate;  
listdelegate = new ListViewDelegate(); 

ui->listView->setItemDelegate(listdelegate);  
ui->listView->setModel(model); 

QStandardItem *item = new QStandardItem();  
item->setData("Some Header",ListViewDelegate::HeaderRole);  
item->setData("Some long description text.",ListViewDelegate::SubheaderRole);  
model->appendRow(item);

Saturday, August 3, 2013

حصن المسلم لسعيد بن علي بن وهف القحطاني للمبرمجين بصيغة txt

هذا الملف بصيغة txt كان ضمن مشروع شخصي أعمل عليه و وجدت أن الفائدة تعمّ بنشره: كتاب "حصن المسلم من أذكار الكتاب و السنة" لسعيد بن علي بن وهف القحطاني. كل سطر من الملف يساوي دعاءً أو ذكرا من الكتاب. حيث يكون بالصيغة التالية:
رقم الفصل|عنوان الفصل|رقم الدعاء|الدعاء

مثال:
7|دعاء الخروج من الخلاء|11|"غُفْرانَكَ".

و في حال وجود أي خطأ إملائي أو غيره أرجو إخباري لتعديله و إعادة رفعه.

Thursday, July 25, 2013

Reply Machine: Static XML/JSON replies for application testing


Update: Reply Machine is now Echo : A free online service to create custom JSON/XML/HTML/Plain replies for application testing.

As I'm moving forward with small applications for learning purposes, here comes a new web application: Reply Machine.



Reply Machine is a free online service to create static XML, JSON, and raw replies for application testing.
This service can help in a situation like when I need to create a simple reply that does not deserve to setup a local service, or when I want to integrate my application with a colleague who knows nothing about web development.

The idea is simple: 
  • Write the reply you wish to receive
  • Set the reply type (xml, json, raw data)
  • Hit the 'create' button.
The output is a link to use in your application for testing

There are some advanced options as well:
  • Delay: In case you want to delay the time between your request and server reply.
  • Encryption: If you have sensitive data in your reply, you can save the reply as an encrypted text and pass the password each time you request the reply. (the output URL has all you need, just try it)

Try it now

Monday, July 22, 2013

Diamond Reaction: An HTML5 Chain Reaction Game using Crafty JS

I've been away from blogging for a long time. Maybe because I've not been learning code-detailed topics. I've been more like learning the basics in topics I've never tries before (like PHP) and learning some theoretical topics.

Anyways, this time I'm trying a new approach to gain more experience. This was I can gain experience from anything I want to learn. So here is my almost-full game implementation of an HTML5 chain reaction game called: Diamond Reaction.

Play Diamond Reaction now



Let's talk a bit about it:

Idea:
I wanted to make a game for fun, with the focus on game mechanics. So I started thinking of some ideas that can be fun for a 'mario-lover' like me. Whenever I think of an idea, I search about it and see how frequent I find the same mechanics on the web. Till I arrived at this concept that did not have a big hit before: chain reaction games.

Theme:
If you look at a game like Angry Birds, you can find the game mechanics are not that hard or new. But the theme is what made it a big hit. I'm doing my game for fun and experience and I'm not a graphic designer, so I started searching for the common simple themes for the popular games these days, especially on Facebook. The most noticed theme was the cartoonish diamonds theme (plus bubbles and animals). So I picked this theme because It will not need much effort to make, plus it actually makes the game more fun to play (you can search for chain reaction games and see how boring their graphics are).

Implementation:
Since the concept of chain reaction game is not that hard to implement, and I already know how to use Crafty JS framework, the game did not take much time to get to the first demo.

Gamification:
I thought about adding a small feature to add more fun and challenge to the game: highscore and leaderoard. but I did not to keep user data on my small server. So I decided to add an optional Facebook login button to login with Facebook credentials so that I can use the Scores API the Facebook has.

Balance:
These kinds of small games can get boring if I did not find the right balance for the game. Since it was a very small game with no big details, fine-tuning was made easily by playing and -trial-and-error.

Missing features:
- No sound was added, maybe I can add it later.
- I'm not satisfied with the current balancing.
- Add more items and surprises to the game to make it more fun and engaging.

Play Diamond Reaction now




Sunday, June 30, 2013

Crafty JS Tutorial (DX Ball / Breakout)



Introduction:

This Crafty JS tutorial is part of Alkottab Studio's internship program, to take developers with no previous game development experience in the quickest way to production phase.

Prerequisites:
  • Previous development experience.
  • The very basics of HTML/CSS/Javascript.
  • Finishing the first part of the Crafty tutorial on its website. [ How Crafty worksDownload and Setup ]

Section 1: Game description:


  • A ball continuously moving and hitting walls and bricks.
  • Bricks are destroyed and points are collected when hit by the ball.
  • User controls a bat and moves it right and left to prevent the ball from hitting the ground.
  • If the ball hits the ground, a life is lost.

Section 2: File Structure:

Before starting the tutorial, it is better to explain how the files are arranged:
  • index file : links to JS files, game div, some simple CSS.
  • js directory: Crafty file and game file.
  • assets directory: images and spritesheets used.

Section 3: Coding considerations:
  • For a cleaner code, some complex game items will be separated as Crafty components to be used later in game scenes.
  • To handle game constants, variables and cross-component calls (like an object wanting to affect the score bar), I will make a simple game manager to contain values and functions connecting between components.

Section 4: Scenes:
  • Loading: A scene only for loading game assets and giving the user a progress indication.
  • Home: Main Menu screen of the game. For now, it will contain only a start button, but it can have more buttons and some graphics representing the game.
  • Game: the scene with the actual play.
  • Total Score: When a user wins or loses, this scene will show the total score.


For the complete tutorial, view the PDF file online (link) or download the PDF + code (link)

Wednesday, May 8, 2013

Facebook Scores API with Koala Gem

Here is a code snippet for a piece of code that I found a lot of people asking about on the web. Since Koala gem does not have an explicit API for Facebook Scores API, some people wonder why it isn't there.

Actually, there is a generic call that can do the job just fine:

After getting your Koala gem to work: check this URL for more about Creating a Facebook Rails app with Koala gem.


Read the set of scores for a user and their friends

Facebook Scores API states that: "You can read the set of scores for a user and their friends for your app by issuing an HTTP GET request to /APP_ID/scores with the user access_token for that app."

So this can be done with the 'get_object()' method,
api = Koala::Facebook::API.new(session[:access_token])
scores = api.get_object(APP_ID + "/scores")


Create or update a score for a user:

Facebook Scores API states that: "You can post a score for a user by issuing an HTTP POST request to /USER_ID/scores with a user or app access_token as long as the user has granted the publish_actions permission for your app."

So this can be done with the 'put_connections()' method,
api = Koala::Facebook::API.new(session[:access_token])
user_profile = api.get_object("me")
result = api.put_connections(user_profile['id'], 'scores?score='+my_score)

if result == true
    #great!
else
    #oops!
end



Friday, April 19, 2013

Qt: Save QPainter Output in an SVG or Image File

To save what you paint in a Qt window to a file (SVG or image) is an easy and straightforward piece of code.

You create a QSvgGenerator or a QImage object.
Some initialization.
Paint into the object (treat it as an IODevice)
And  we are done.

Suppose that this is what I draw in my paint event:

void MainWindow::paintEvent(QPaintEvent *)
{
    QPainter painter;
    painter.begin(this);
    
    painter.setRenderHint(QPainter::Antialiasing);
    paint(painter);
    
    painter.end();
}

void MainWindow::paint(QPainter &painter)
{
    painter.setClipRect(QRect(0, 0, 200, 200));
    painter.setPen(Qt::NoPen);
    painter.fillRect(QRect(0, 0, 200, 200), Qt::gray);
    painter.setPen(QPen(Qt::white, 4, Qt::DashLine));
    painter.drawLine(QLine(0, 35, 200, 35));
    painter.drawLine(QLine(0, 165, 200, 165));
}


If I want to save it into an SVG file, it would be something like this

void MainWindow::saveSvg()
{
    QString path = QFileDialog::getSaveFileName(this, tr("Save as SVG"),"", tr("SVG file (*.svg)"));

    if (path.isEmpty())
        return;

    QSvgGenerator generator;
    generator.setFileName(path);
    generator.setSize(QSize(200, 200));
    generator.setViewBox(QRect(0, 0, 200, 200));
    generator.setTitle(tr("SVG Generator Example Drawing"));
    generator.setDescription(tr("An SVG drawing created by the SVG Generator "
                             "Example provided with Qt."));
    QPainter painter;
    painter.begin(&generator);
    paint(painter);
    painter.end();
}


If I want to save it into an image file, it would be something like this.

void MainWindow::savePng()
{
    QString path = QFileDialog::getSaveFileName(this, tr("Save as image"), "", tr("PNG file (*.png)"));

    if (path.isEmpty())
        return;

    QImage img(200, 200, QImage::Format_ARGB32);

    QPainter painter;
    painter.begin(&img);
    paint(painter);
    painter.end();

    img.save(path);
}

Note that if you  replace
QImage img(200, 200, QImage::Format_ARGB32);

QPainter painter;
painter.begin(&img);
paint(painter);
painter.end();
with
QImage img(this->size(), QImage::Format_ARGB32);
QPainter painter(&img);
this->render(&painter);
you'll be taking a screenshot of widget content into an image.


References:
SVG Generator Example
Capture Qt widget as an image file

Thursday, April 11, 2013

Resizable Google Chart

To resize a Google chart (pie chart, area chart, geo chart ...) is a matter of a few lines of code after refactoring the given example in the Google Charts Gallery. The keyword is: window resize.

Just keep your data in a javascript variable and put the Google Chart code in a function to be called in each onResize event. That way the chart will be redraw to fit in the new size of the chart div, and it will look like it is resizing / scaling.

Note that you may want to set a min/max width/height for the size of the chart (css on the div can do the job just fine).

Performance? Just fine. Unless you have large data and many details to draw, then it may be a headache to draw from the beginning at each resize (you can limit the chart resize frequency with javascript by a delay or timer).

Here is a complete example:

<html>
  <head>
    <script type="text/javascript" src="https://www.google.com/jsapi"></script>
    <script type="text/javascript">
    window.onresize = function(){
        startDrawingChart();
    };

    window.onload = function(){
        startDrawingChart();
    };

    var data_array = [
                      ['Year', 'Sales', 'Expenses'],
                      ['2004',  1000,      400],
                      ['2005',  1170,      460],
                      ['2006',  660,       1120],
                      ['2007',  1030,      540]
                    ];
                    
    startDrawingChart = function(){
        google.load("visualization", "1", {packages:["corechart"],callback: drawChart});

        function drawChart() {
            var data = google.visualization.arrayToDataTable(data_array);

            var options = {
              title: 'Company Performance',
              hAxis: {title: 'Year',  titleTextStyle: {color: 'red'}}
            };

            var chart = new google.visualization.AreaChart(document.getElementById('chart_div'));
            chart.draw(data, options);
        }
    };
    </script>
  </head>
  <body>
    <div id="chart_div"></div>
  </body>
</html>

Tuesday, April 9, 2013

Camera Drag and Zoom with Mouse in Unity 3D

The script is easy and descriptive, it is the result of some searching and modification.

  • Press the left mouse button and drag to move, and use mouse scroll wheel to zoom in and out.
  • The zooming code can work for both orthogonal and perspective but I've been using and testing it in orthogonal mode. 
  • Modify the public values to fit in your game.

** Just drag the script to your camera and it will work! **

C# code
[CameraDragZoom.cs]
using UnityEngine;
using System.Collections;

public class CameraDragZoom : MonoBehaviour {
    
    public float dragSpeed = -10;
    public int minX = -892;
    public int maxX = 1111;
    public int minZ = -880;
    public int maxZ = 1145;
    
    public int bottomMargin = 80; // if you have some icons at the bottom (like an RPG game) this will help preventing the drag action at the bottom
    
    public float orthZoomStep = 10.0f;
    public int orthZoomMaxSize = 500;
    public int orthZoomMinSize = 300;
    
    private bool orthographicView = true;
    private Vector3 dragOrigin;
    
    // Update is called once per frame
    void Update () {
        moveCamera();
        zoomCamera();
    }
    
    void moveCamera()
    {
        if (Input.GetMouseButtonDown(0))
        {    
            dragOrigin = Input.mousePosition;
            return;
        }

        if (!Input.GetMouseButton(0)) return;
        
        if(dragOrigin.y <= bottomMargin) return;
        
        Vector3 pos = Camera.main.ScreenToViewportPoint(Input.mousePosition - dragOrigin);
        Vector3 move = new Vector3(pos.x * dragSpeed, 0, pos.y * dragSpeed);
                
        if(move.x > 0)
        {
            if(!isWithinRightBorder())
                move.x =0;
        }
        else
        {
            if(!isWithinLeftBorder())
                move.x=0;
        }
        
        if(move.z > 0)
        {
            if(!isWithinTopBorder())
                move.z=0;
        }
        else
        {
            if(!isWithinBottomBorder())
                move.z=0;
        }
            
        
        transform.Translate(move, Space.World);
    }
    
    void zoomCamera()
    {
        if(!isWithinBorders())
            return;
        
        // zoom out
        if (Input.GetAxis("Mouse ScrollWheel") <0)
        {
            if(orthographicView)
            {
                if (Camera.main.orthographicSize <=orthZoomMaxSize)
                    Camera.main.orthographicSize += orthZoomStep;
            }
            else
            {
                if (Camera.main.fieldOfView<=150)
                       Camera.main.fieldOfView +=5;
            }
        }
        // zoom in
        if (Input.GetAxis("Mouse ScrollWheel") > 0)
           {
            if(orthographicView)
            {
                if (Camera.main.orthographicSize >= orthZoomMinSize)
                     Camera.main.orthographicSize -= orthZoomStep;            
            }
            else
            {
                if (Camera.main.fieldOfView>2)
                    Camera.main.fieldOfView -=5;
            }
           }
    }
    
    bool isWithinBorders()
    {
        return ( isWithinLeftBorder() && isWithinBottomBorder() && isWithinRightBorder() && isWithinTopBorder() );
    }
    
    bool isWithinLeftBorder()
    {
        Vector3 currentTopLeftGlobal = Camera.main.ScreenToWorldPoint(new Vector3(0,0,0));
        if(currentTopLeftGlobal.x > minX)
            return true;
        else
            return false;
        
    }
    
    bool isWithinRightBorder()
    {
        Vector3 currentBottomRightGlobal = Camera.main.ScreenToWorldPoint(new Vector3(Screen.width,0,0));
        if(currentBottomRightGlobal.x < maxX)
            return true;
        else
            return false;
    }
    
    bool isWithinTopBorder()
    {
        Vector3 currentTopLeftGlobal = Camera.main.ScreenToWorldPoint(new Vector3(0,Screen.height,0));
        if(currentTopLeftGlobal.z < maxZ)
            return true;
        else
            return false;
    }
    
    bool isWithinBottomBorder()
    {
        Vector3 currentBottomRightGlobal = Camera.main.ScreenToWorldPoint(new Vector3(Screen.width,0,0));
        if(currentBottomRightGlobal.z > minZ)
            return true;
        else
            return false;
    }
}



Monday, February 25, 2013

How to Install Twitter Bootstrap in Rails

To install Twitter Bootstrap in a Rails application, there are several ways and gems. Here I prefer the working and straight-forward one.

1- Inside "group :assets" add:
gem 'bootstrap-sass'

2- Go to "app\assets\stylesheets" directory and create a file named "bootstrap_and_overrides.css.scss"

3- Inside the previous file, add:
@import "bootstrap";
@import "bootstrap-responsive";

4- Inside "app\assets\javascripts\application.js" file, add:
//= require bootstrap

5- Then in the terminal, do "bundle install" and your Rails application with be ready for Twitter Bootstrap tags.


Friday, February 15, 2013

Unity3D: Rotate Object to Face Another

I've been working on porting an HTML5 demo to Unity3D, when I stopped for more than a day just to rotate a cannon towards a point!

I have to admit that Unity3D is great, but it can turn simple tasks into complex ones for no reason. Anyways, after some time with Quaternion class and Lerp function and searching for online solutions, I decided to go back to basics. The problem with Lerp is that it forces rotation in a certain time. So the cannon will rotate 30 degrees in -say- 5 seconds, and will also rotate 60 degrees in 5 seconds, which is not logical. And to solve this I have to do some extra math.

So what did I do? I just rotated with a certain amount of degrees per second (using rotate() function) , and checked if the angle between my cannon and the target is within the allowed range to shoot (with a cross product of two vectors). That's it!

The code works as follows:
- If cannon is in attack mode, rotate towards target point with a fixed speed.
- The cannon object has a child object called nose, it is a point representing the missile start point and used for measuring the angle between the cannon-nose vector and nose-target vector.
- If the angle is within an accepted value, stop and fire.
- The assumption is that the game is in the Z-plane and the cannon rotates around the Z axis.

public float rotationSpeed = 30;
public float rotationErrorFraction = 0.01f;
bool attackMode = false;
Vector3 targetPoint;

void Update()
{
 
 if(attackMode)
 {   
  Transform nose = transform.FindChild("Nose");

  // check if cannon looks at target
  Vector3 targetDirection = (targetPoint - nose.position);
  targetDirection.z = 0;
  Vector3 cannonDirection = (nose.position - transform.position);
  cannonDirection.z = 0;
  
  targetDirection.Normalize();
  cannonDirection.Normalize();
  Vector3 cross = Vector3.Cross(cannonDirection, targetDirection);
  float crossZ = cross.z;

  // If within range, fire. Else, rotate again.
  if(crossZ < rotationErrorFraction && crossZ > -rotationErrorFraction)
  {
   // fire
   if(missile)
   {
    Instantiate(missile, nose.position, nose.rotation);
   }
   attackMode = false;
  }
  else
  {
   if(crossZ > 0)
    transform.Rotate(Vector3.forward, Time.deltaTime*rotationSpeed);
   else
    transform.Rotate(Vector3.back, Time.deltaTime*rotationSpeed);
  }
 }
}

Thursday, January 10, 2013

Link Preview using Rails, AJAX, and Nokogiri Gem

I was trying to make some working code to preview link content like Facebook, Google Plus, LinkedIn .. etc. And I have to say, it is not that easy to get it to perfection.



Here are some notes before starting:

  • The happy scenario is to find ready tags inside the HEAD tag of the HTML file. Some times they are there, a lot of times they are not there. In video sharing websites like Youtube or Vimeo, or other news websites who care about these details, you will find meta data to save the day. For example:

<meta property="og:url" content="http://www.youtube.com/watch?v=c6nzShZQBLQ">
<meta property="og:title" content="Kinetic Scrolling Example [Arabic] [Qt]">
<meta property="og:description" content="Visit my blog entry for more info, and the complete example: http://3adly.blogspot.com/2010/11/qt-kinetic-scrolling-ariyas-example.html Example uploaded on M...">
<meta property="og:type" content="video">
<meta property="og:image" content="https://i4.ytimg.com/vi/c6nzShZQBLQ/mqdefault.jpg">
<meta property="og:video" content="http://www.youtube.com/v/c6nzShZQBLQ?autohide=1&amp;version=3">
<meta property="og:video:type" content="application/x-shockwave-flash">
<meta property="og:video:width" content="640">
<meta property="og:video:height" content="480">
<meta property="og:site_name" content="YouTube">

  • If these data exist, your work is done. Otherwise, you will have to search inside the HTML document to get some text and images to use for the preview. This mean more calculations and algorithms.
  • A major issue is that Javascript has security issues preventing the process of fetching HTML content of another domain, so the processing on the client side is not possible, and all the parsing has to be on the server side, which means loading the server for a simple feature.
  • Another major issue is that Ruby has no built-in HTML/XML parser. So it is up to you to make your own or search for an alternative. I saved time and used Nokogiri gem to parse HTML and get the data I need.
  • Note that returning HTML data via AJAX is not preferred and can easily break the code. So you'd better return a JSON object and process it on client side. 
  • One final note is that you should take care of text encoding. For example, I prefer to make Arabic support inside my application, so the UTF-8 encoding is important to me.


So here is how the process goes:
  1. Receive pasted URL using Javascript.
  2. Send URL in an AJAX request to your server.
  3. Fetch the HTML content of the URL and parse the useful data.
  4. Send data back to HTML page as a JSON object.
  5. Process the JSON object by Javascript to preview data to user.


1- Receive pasted URL using Javascript:

Javascript does this job. I simply listen to the 'paste' event then get the text inside the textarea. Of course it would be much recommended to validate text first.

$("#post_content").bind('paste', function(e) {
    var el = $(this);
    setTimeout(function() {
        var text = $(el).val();
        // send text to server
    }, 100);
});


2- Send URL in an AJAX request to your server:

$("#post_content").bind('paste', function(e) {
    var el = $(this);

    setTimeout(function() {
        var text = $(el).val();
        
        // send url to service for parsing
        $.ajax('/url/to/server/handler', {
            type: 'POST',
            data: { url: text },
            success: function(data,textStatus,jqXHR ) {
                // handle received data
            },
            error: function() { alert("error"); }
        });
    }, 100);
});


3- Fetch the HTML content of the URL and parse the useful data:
4- Send data back to HTML page as a JSON object:

In this step I use Nokogiri gem to do the dirty work of parsing for me. First remember to add the gem to the Gemfile.
Note: In case of Linux, you may want to install libxslt-dev and libxml2-div before bundling.

gem 'nokogiri' , '~> 1.5.6'

The "param_url"  is the url received on the server side. I pass it to Nokogiri then play with the document object returned. The easiest way is to iterate on the mate tags in HEAD and get the strings I want. Here you may make more effort to parse data from the BODY if the meta tags were not helpful.

doc = Nokogiri::HTML(open(param_url), nil, 'UTF-8')
            
title = ""
description = ""
url = ""
image_url = ""

doc.xpath("//head//meta").each do |meta|
    if meta['property'] == 'og:title'
        title = meta['content']
    elsif meta['property'] == 'og:description' || meta['name'] == 'description'
        description = meta['content']
    elsif meta['property'] == 'og:url'
        url = meta['content']
    elsif meta['property'] == 'og:image'
        image_url = meta['content']
    end
end

if title == ""
    title_node = doc.at_xpath("//head//title")
    if title_node
        title = title_node.text
    elsif doc.title
        title = doc.title
    else
        title = param_url
    end
end

if description ==""
    #maybe search for content from BODY
    description = title
end

if url ==""
    url = param_url
end

render :json => {:title => title, :description => description, :url => url, :image_url => image_url} and return


5- Process the JSON object by Javascript to preview data to user:

Finally, the data is returned to the Javascript on the client side. Be creative with handling the data and viewing it to the user. Here is a single line as an example of handling data inside the 'success' handler.

$("#preview-title").text(data['title']);