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