1 /****************************************************************************
2 **
3 ** Copyright (C) 2006-2009 fullmetalcoder <fullmetalcoder@hotmail.fr>
4 **
5 ** This file is part of the Edyuk project <http://edyuk.org>
6 **
7 ** This file may be used under the terms of the GNU General Public License
8 ** version 3 as published by the Free Software Foundation and appearing in the
9 ** file GPL.txt included in the packaging of this file.
10 **
11 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
12 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
13 **
14 ****************************************************************************/
15 
16 #include "qeditsession.h"
17 
18 /*!
19 	\file qeditsession.cpp
20 	\brief Implementation of the QEditSession class.
21 */
22 
23 #include "qeditor.h"
24 #include "qdocument.h"
25 #include "qdocument_p.h"
26 #include "qdocumentline.h"
27 #include "qdocumentcursor.h"
28 
29 #include "qlinemarksinfocenter.h"
30 
31 #include <QFile>
32 #include <QFileInfo>
33 #include <QDataStream>
34 #include <QScrollBar>
35 
36 /*!
37 	\class QEditSession
38 
39 	\brief A session recording class
40 
41 	The purpose of this class is to collect session data from several QEditor object,
42 	to serialize it and to re-create the same session by deserializing the stored data.
43 */
44 
45 /*!
46 	\brief ctor
47 */
QEditSession(QObject * p)48 QEditSession::QEditSession(QObject *p)
49  : QObject(p), m_id(-1), m_delay(0)
50 {
51 	setFormat(FormatCount - 1);
52 }
53 
54 /*!
55 	\brief ctor
56 */
QEditSession(const QString & f,QObject * p)57 QEditSession::QEditSession(const QString& f, QObject *p)
58  : QObject(p), m_id(-1), m_delay(0)
59 {
60 	setFileName(f);
61 	setFormat(FormatCount - 1);
62 }
63 
64 /*!
65 	\brief dtor
66 */
~QEditSession()67 QEditSession::~QEditSession()
68 {
69 
70 }
71 
72 /*!
73 	\return The update interval, in milliseconds
74 
75 	A value of zero means the data is NOT automatically updated.
76 
77 	\see updateData()
78 */
autoUpdateInterval() const79 int QEditSession::autoUpdateInterval() const
80 {
81 	return m_delay;
82 }
83 
84 /*!
85 	\brief Set the update interval
86 
87 	If \a ms is strictly positive then the data will be
88 	updated every \a ms milliseconds
89 
90 	If the session has been given a valid filename, the updated
91 	data will automatically be saved to that file.
92 */
setAutoUpdateInterval(int ms)93 void QEditSession::setAutoUpdateInterval(int ms)
94 {
95 	if ( m_delay )
96 	{
97 		killTimer(m_id);
98 		m_id = -1;
99 	}
100 
101 	m_delay = ms;
102 
103 	if ( m_delay )
104 	{
105 		m_id = startTimer(m_delay);
106 	}
107 }
108 
109 /*!
110 	\return The format used to save session data
111 */
format() const112 int QEditSession::format() const
113 {
114 	return m_format;
115 }
116 
117 /*!
118 	\brief Set the session save format
119 
120 	QEditSession maintains full backward compatibility for session
121 	loading, automatically determining the session format and using
122 	proper loader. However it will automatically convert session
123 	files when saving (to the latest format by default). If you want
124 	to enforce backward compat of session files with older releases
125 	you may want to manually set the session save format.
126 */
setFormat(int fmt)127 void QEditSession::setFormat(int fmt)
128 {
129 	m_format = fmt;
130 }
131 
132 /*!
133 	\return The file name used as storage
134 
135 	If it is empty then no auto-save is performed.
136 */
fileName() const137 QString QEditSession::fileName() const
138 {
139 	return m_fileName;
140 }
141 
142 /*!
143 	\brief Set the storage destination
144 
145 	Every time the data is updated, either when the autoupdate timer
146 	ticks or when updateData() is called programmatically, it will be
147 	written to that file, provided the filename is valid and points
148 	to a writeable location.
149 
150 	\see updateData()
151 	\see autoUpdateInterval()
152 */
setFileName(const QString & filename,bool r)153 void QEditSession::setFileName(const QString& filename, bool r)
154 {
155 	m_fileName = filename;
156 
157 	if ( r )
158 		restore();
159 
160 }
161 
162 /*!
163 	\brief Add an editor to the session
164 */
addEditor(QEditor * e)165 void QEditSession::addEditor(QEditor *e)
166 {
167 	if ( m_editors.contains(e) )
168 		return;
169 
170 	//qDebug("+ 0x%x", e);
171 
172 	Document *d = new Document;
173 	d->flags = 0;
174 
175 	m_editors << e;
176 	m_sessionData << d;
177 
178 	connect(e	, SIGNAL( destroyed(QObject*) ),
179 			this, SLOT  ( destroyed(QObject*) ) );
180 
181 	connect(e	, SIGNAL( saved(QEditor*, QString) ),
182 			this, SLOT  ( saved(QEditor*, QString) ) );
183 
184 	connect(e	, SIGNAL( loaded(QEditor*, QString) ),
185 			this, SLOT  ( loaded(QEditor*, QString) ) );
186 
187 	update(e, d);
188 }
189 
190 /*!
191 	\brief Remove an editor from the session
192 */
removeEditor(QEditor * e)193 void QEditSession::removeEditor(QEditor *e)
194 {
195 	int idx = m_editors.indexOf(e);
196 
197 	if ( idx == -1 )
198 		return;
199 
200 	//qDebug("- 0x%x", e);
201 
202 	disconnect(	e	, SIGNAL( destroyed(QObject*) ),
203 				this, SLOT  ( destroyed(QObject*) ) );
204 
205 	disconnect(	e	, SIGNAL( saved(QEditor*, QString) ),
206 				this, SLOT  ( saved(QEditor*, QString) ) );
207 
208 	disconnect(	e	, SIGNAL( loaded(QEditor*, QString) ),
209 				this, SLOT  ( loaded(QEditor*, QString) ) );
210 
211 	m_editors.removeAt(idx);
212 	delete m_sessionData.takeAt(idx);
213 }
214 
215 /*!
216 
217 */
clear(bool cleanup)218 void QEditSession::clear(bool cleanup)
219 {
220 	if ( cleanup )
221 		qDeleteAll(m_editors);
222 
223 	qDeleteAll(m_sessionData);
224 
225 	m_editors.clear();
226 	m_sessionData.clear();
227 }
228 
229 /*!
230 	\brief Serialize session data
231 */
save()232 void QEditSession::save()
233 {
234 	QFile f(m_fileName);
235 
236 	if ( f.open(QFile::WriteOnly) )
237 	{
238 		QDataStream s(&f);
239 
240 		save(s);
241 	}
242 }
243 
244 /*!
245 	\brief Serialize session data
246 */
restore()247 void QEditSession::restore()
248 {
249 	QFile f(m_fileName);
250 
251 	if ( f.open(QFile::ReadOnly) )
252 	{
253 		QDataStream s(&f);
254 
255 		restore(s);
256 	}
257 }
258 
259 typedef void (*save_func)(QDataStream&, const QList<QEditSession::Document*>&);
260 typedef QList<QEditSession::Document*> (*restore_func)(QDataStream&);
261 
262 QList<QEditSession::Document*> restore_qes(QDataStream&);
263 void save_qes(QDataStream&, const QList<QEditSession::Document*>&);
264 
265 QList<QEditSession::Document*> restore_qes1(QDataStream&);
266 void save_qes1(QDataStream&, const QList<QEditSession::Document*>&);
267 
268 // magic numbers (4 letters, 4 bytes, 32bit integer...)
269 /*
270 
271 */
272 static QList<const char*> _magic = QList<const char*>()
273 	<< "QES "
274 	<< "QES1";
275 
276 static QList<save_func> _save = QList<save_func>()
277 	<< save_qes
278 	<< save_qes1;
279 
280 static QList<restore_func> _restore = QList<restore_func>()
281 	<< restore_qes
282 	<< restore_qes1;
283 
284 ///////////////////////////////////////////////////////////////////////////////
285 
286 /*!
287 	\brief Serialize session data
288 */
save(QDataStream & s)289 void QEditSession::save(QDataStream& s)
290 {
291 	int format = qBound(0, m_format, FormatCount - 1);
292 
293 	s.writeRawData(_magic.at(format), 4);
294 
295 	//qDebug("saving session [fmt:%i]", format);
296 	_save.at(format)(s, m_sessionData);
297 }
298 
299 /*!
300 	\brief Deserialize session data
301 */
restore(QDataStream & s)302 void QEditSession::restore(QDataStream& s)
303 {
304 	//qDebug("restoring");
305 
306 	char magic[4];
307 
308 	s.readRawData(magic, 4);
309 
310 	for ( int i = 0; i < _magic.count(); ++i )
311 	{
312 		const char *mi = _magic.at(i);
313 		bool matchMagic, normal = true, wicked = true;
314 
315 		for ( int j = 0; j < 4; ++j )
316 		{
317 			normal &= magic[j] == mi[j];
318 			wicked &= magic[j] == mi[3 - j];
319 		}
320 
321 		matchMagic = i ? normal : normal | wicked;
322 
323 		if ( matchMagic )
324 		{
325 			//qDebug("loading session [fmt:%i]", i);
326 
327 			QList<Document*> docs = _restore.at(i)(s);
328 
329 			foreach ( Document *d, docs )
330 			{
331 				if ( QFile::exists(d->fileName) )
332 				{
333 					QEditor *e = createEditor();
334 
335 					e->load(d->fileName);
336 
337 					QDocumentCursor c = d->cursors.first().toDocumentCursor(e->document());
338 
339 					if ( d->cursors.count() )
340 					{
341 						e->setCursor(c);
342 
343 						for ( int j = 1; j < d->cursors.count(); ++j )
344 							e->addCursorMirror(d->cursors.at(j).toDocumentCursor(e->document()));
345 					}
346 
347 					QHash<int, QList<int> >::const_iterator it = d->marks.constBegin();
348 
349 					while ( it != d->marks.constEnd() )
350 					{
351 						QList<int> marks = *it;
352 
353 						foreach ( int mark, marks )
354 							e->document()->line(it.key()).addMark(mark);
355 					}
356 
357 					// TODO : defer. it does not seem to work properly that way
358 					// TODO : view size independency (store the first visible line number)
359 					e->verticalScrollBar()->setValue(d->scrollY);
360 					e->horizontalScrollBar()->setValue(d->scrollX);
361 
362 					connect(e	, SIGNAL( destroyed(QObject*) ),
363 							this, SLOT  ( destroyed(QObject*) ) );
364 
365 					connect(e	, SIGNAL( saved(QEditor*, QString) ),
366 							this, SLOT  ( saved(QEditor*, QString) ) );
367 
368 					connect(e	, SIGNAL( loaded(QEditor*, QString) ),
369 							this, SLOT  ( loaded(QEditor*, QString) ) );
370 
371 					m_editors << e;
372 					m_sessionData << d;
373 
374 					emit restored(e);
375 				} else {
376 					delete d;
377 				}
378 			}
379 
380 			return;
381 		}
382 	}
383 
384 	qWarning("Unsupported session format");
385 }
386 
387 /*!
388 	\brief Updates the data
389 
390 	Fetches up-to-date session data from the attached editors.
391 
392 	If the session has been given a valid filename the data will
393 	automatically be saved.
394 
395 	\note This will NOT affect the automatic updates timing
396 */
updateData()397 void QEditSession::updateData()
398 {
399 	for ( int i = 0; i < m_editors.count(); ++i )
400 	{
401 		QEditor *e = m_editors.at(i);
402 		Document *d = m_sessionData.at(i);
403 
404 		update(e, d);
405 	}
406 
407 	save();
408 }
409 
destroyed(QObject * o)410 void QEditSession::destroyed(QObject *o)
411 {
412 	//qDebug("~ 0x%x", o);
413 
414 	for ( int i = 0; i < m_editors.count(); ++i )
415 	{
416 		QEditor *e = m_editors.at(i);
417 
418 		if ( !e || ((QObject*)e == o) )
419 		{
420 			delete m_sessionData.takeAt(i);
421 			m_editors.removeAt(i);
422 			break;
423 		}
424 	}
425 }
426 
427 /*!
428 	\brief Called whenever an editor is saved
429 
430 	This handler is responsible for updating file names and time stamps
431 	which is needed to avoid data loss upon session restoration
432 */
saved(QEditor * e,const QString & fn)433 void QEditSession::saved(QEditor *e, const QString& fn)
434 {
435 	int idx = m_editors.indexOf(e);
436 
437 	if ( idx == -1 )
438 		return;
439 
440 	//qDebug("saved : %s", qPrintable(fn));
441 
442 	Document *d = m_sessionData.at(idx);
443 
444 	//d->timeStamp = QDateTime::currentDateTime();
445 
446 	update(e, d);
447 }
448 
449 /*!
450 	\brief Called whenever an editor is loaded with new content
451 
452 	This handler is responsible for updating file names and time stamps
453 	which is needed to avoid data loss upon session restoration
454 */
loaded(QEditor * e,const QString & fn)455 void QEditSession::loaded(QEditor *e, const QString& fn)
456 {
457 	int idx = m_editors.indexOf(e);
458 
459 	if ( idx == -1 )
460 		return;
461 
462 	//qDebug("loaded : %s", qPrintable(fn));
463 
464 	Document *d = m_sessionData.at(idx);
465 
466 	//d->timeStamp = QDateTime::currentDateTime();
467 
468 	update(e, d);
469 }
470 
update(QEditor * e,Document * d)471 void QEditSession::update(QEditor *e, Document *d)
472 {
473 	if ( !e || !d )
474 		return;
475 
476 	//qDebug(">>%s", qPrintable(e->fileName()));
477 
478 	d->fileName = e->fileName();
479 	d->timeStamp = QFileInfo(d->fileName).lastModified();
480 
481 	d->cursors.clear();
482 	d->cursors << Cursor(e->cursor());
483 
484 	for ( int i = 0; i < e->cursorMirrorCount(); ++i )
485 		d->cursors << Cursor(e->cursorMirror(i));
486 
487 	d->marks.clear();
488 
489 	QLineMarkList marks = QLineMarksInfoCenter::instance()->marks(d->fileName);
490 
491 	foreach ( const QLineMark& mark, marks )
492 	{
493 		d->marks[mark.line] << mark.mark;
494 	}
495 
496 	d->scrollX = e->verticalScrollBar()->value();
497 	d->scrollY = e->horizontalScrollBar()->value();
498 }
499 
500 /*!
501 	\internal
502 */
timerEvent(QTimerEvent * e)503 void QEditSession::timerEvent(QTimerEvent *e)
504 {
505 	if ( e->timerId() == m_id )
506 	{
507 		updateData();
508 	}
509 }
510 
createEditor()511 QEditor* QEditSession::createEditor()
512 {
513 	return new QEditor;
514 }
515 
Cursor(const QDocumentCursor & c)516 QEditSession::Cursor::Cursor(const QDocumentCursor& c)
517 {
518 	beginLine = c.lineNumber();
519 	beginColumn = c.columnNumber();
520 	endLine = c.hasSelection() ? c.anchorLineNumber() : -1;
521 	endColumn = c.hasSelection() ? c.anchorColumnNumber() : -1;
522 
523 	//qDebug("((%i, %i), (%i, %i))", beginLine, beginColumn, endLine, endColumn);
524 }
525 
toDocumentCursor(QDocument * d) const526 QDocumentCursor QEditSession::Cursor::toDocumentCursor(QDocument *d) const
527 {
528 	//qDebug("((%i, %i), (%i, %i))", beginLine, beginColumn, endLine, endColumn);
529 
530 	QDocumentCursor beg(d, beginLine, beginColumn);
531 	QDocumentCursor end(d, endLine, endColumn);
532 
533 	if ( endLine != -1 )
534 	{
535 		end.setSelectionBoundary(beg);
536 		return end;
537 	}
538 
539 	return beg;
540 }
541 
542 ///////////////////////////////////////////////////////////////////////////////
543 
restore_qes(QDataStream & s)544 QList<QEditSession::Document*> restore_qes(QDataStream& s)
545 {
546 	QList<QEditSession::Document*> l;
547 	int documentCount = 0;
548 
549 	s >> documentCount;
550 
551 	for ( int i = 0; i < documentCount; ++i )
552 	{
553 		QEditSession::Document *d = new QEditSession::Document;
554 		d->flags = 0;
555 
556 		s >> d->fileName;
557 		s >> d->timeStamp;
558 
559 		//qDebug("> %s", qPrintable(d->fileName));
560 
561 		int cursorCount = 0;
562 
563 		s >> cursorCount;
564 
565 		for ( int j = 0; j < cursorCount; ++j )
566 		{
567 			QEditSession::Cursor c;
568 
569 			s >> c.beginLine;
570 			s >> c.beginColumn;
571 			s >> c.endLine;
572 			s >> c.endColumn;
573 
574 			d->cursors << c;
575 		}
576 
577 		int markCount = 0;
578 		s >> markCount;
579 
580 		for ( int j = 0; j < markCount; ++j )
581 		{
582 			int line = 0;
583 			QList<int> marks;
584 
585 			s >> line;
586 			s >> marks;
587 
588 			d->marks[line] = marks;
589 
590 		}
591 
592 		s >> d->scrollX;
593 		s >> d->scrollY;
594 
595 		l << d;
596 	}
597 
598 	return l;
599 }
600 
save_qes(QDataStream & s,const QList<QEditSession::Document * > & m_sessionData)601 void save_qes(QDataStream& s, const QList<QEditSession::Document*>& m_sessionData)
602 {
603 	s << m_sessionData.count();
604 
605 	foreach ( QEditSession::Document *d, m_sessionData )
606 	{
607 		//qDebug("> %s", qPrintable(d->fileName));
608 
609 		s << d->fileName;
610 		s << d->timeStamp;
611 
612 		s << d->cursors.count();
613 
614 		foreach ( const QEditSession::Cursor& c, d->cursors )
615 			s << c.beginLine << c.beginColumn << c.endLine << c.endColumn;
616 
617 		s << d->marks.count();
618 
619 		QHash<int, QList<int> >::const_iterator it = d->marks.constBegin();
620 		const QHash<int, QList<int> >::const_iterator end = d->marks.constEnd();
621 
622 		while ( it != end )
623 		{
624 			s << it.key() << *it;
625 			++it;
626 		}
627 
628 		s << d->scrollX;
629 		s << d->scrollY;
630 	}
631 }
632 
633 ///////////////////////////////////////////////////////////////////////////////
634 
restore_qes1(QDataStream & s)635 QList<QEditSession::Document*> restore_qes1(QDataStream& s)
636 {
637 	QList<QEditSession::Document*> l;
638 
639 	quint32 version = 0;
640 	s >> version;
641 
642 	if ( version )
643 	{
644 		qWarning("Unsupported version of session format (QCESession1) : possible data loss");
645 	}
646 
647 	quint32 documentCount = 0;
648 	s >> documentCount;
649 
650 	for ( quint32 i = 0; i < documentCount; ++i )
651 	{
652 		QEditSession::Document *d = new QEditSession::Document;
653 
654 		s >> d->flags;
655 		s >> d->fileName;
656 		s >> d->timeStamp;
657 
658 		//qDebug("> %s", qPrintable(d->fileName));
659 
660 		quint32 cursorCount = 0;
661 
662 		s >> cursorCount;
663 
664 		for ( quint32 j = 0; j < cursorCount; ++j )
665 		{
666 			QEditSession::Cursor c;
667 
668 			s >> c.beginLine;
669 			s >> c.beginColumn;
670 			s >> c.endLine;
671 			s >> c.endColumn;
672 
673 			d->cursors << c;
674 		}
675 
676 		quint32 markCount = 0;
677 		s >> markCount;
678 
679 		for ( quint32 j = 0; j < markCount; ++j )
680 		{
681 			int line = 0;
682 			QList<int> marks;
683 
684 			s >> line;
685 			s >> marks;
686 
687 			d->marks[line] = marks;
688 		}
689 
690 		s >> d->scrollX;
691 		s >> d->scrollY;
692 
693 		// handle extra (version-specific) fields here
694 
695 		qint32 extra_sz;
696 		s >> extra_sz;
697 
698 		// make sure we do not mistake extra fields as data for another document
699 		s.skipRawData(extra_sz);
700 
701 
702 		l << d;
703 	}
704 
705 	return l;
706 }
707 
save_qes1(QDataStream & s,const QList<QEditSession::Document * > & m_sessionData)708 void save_qes1(QDataStream& s, const QList<QEditSession::Document*>& m_sessionData)
709 {
710 	bool supportExtra = !s.device()->isSequential();
711 
712 	s << quint32(0);
713 
714 	s << m_sessionData.count();
715 
716 	foreach ( QEditSession::Document *d, m_sessionData )
717 	{
718 		//qDebug("> %s", qPrintable(d->fileName));
719 
720 		s << d->flags;
721 		s << d->fileName;
722 		s << d->timeStamp;
723 
724 		s << d->cursors.count();
725 
726 		foreach ( const QEditSession::Cursor& c, d->cursors )
727 			s << c.beginLine << c.beginColumn << c.endLine << c.endColumn;
728 
729 		s << d->marks.count();
730 
731 		QHash<int, QList<int> >::const_iterator it = d->marks.constBegin();
732 		const QHash<int, QList<int> >::const_iterator end = d->marks.constEnd();
733 
734 		while ( it != end )
735 		{
736 			s << it.key() << *it;
737 			++it;
738 		}
739 
740 		s << d->scrollX;
741 		s << d->scrollY;
742 
743 		// extra data
744 		if ( supportExtra )
745 		{
746 			// make room for size field and record position
747 			qint64 sz_pos = s.device()->pos();
748 			s << qint32(0);
749 			qint64 post_sz_pos = s.device()->pos();
750 
751 
752 			// write extra fields here
753 
754 			//s << "some crap to test extra fields.";
755 
756 
757 
758 			// update the value of the size field based on what was written
759 			qint64 extra_end = s.device()->pos();
760 			s.device()->seek(sz_pos);
761 			s << qint32(extra_end - post_sz_pos);
762 			s.device()->seek(extra_end);
763 
764 			//qDebug("%i, %i, %i", qint32(sz_pos), qint32(post_sz_pos), qint32(extra_end));
765 		} else {
766 			// no extra fields
767 			s << qint32(0);
768 
769 
770 		}
771 	}
772 }
773