Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
event-log.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Author:
4 * Gustav Broberg <broberg@kth.se>
5 * Jon A. Cruz <jon@joncruz.org>
6 *
7 * Copyright (c) 2014 Authors
8 *
9 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
10 */
11
12#include "event-log.h"
13
14
15#include <glibmm/i18n.h>
16
17#include "document.h"
19#include "util/signal-blocker.h"
20
21namespace
22{
23
24class DialogConnection
25{
26public:
27 DialogConnection(Gtk::TreeView *event_list_view, Inkscape::EventLog::CallbackMap *callback_connections) :
28 _event_list_view(event_list_view),
29 _callback_connections(callback_connections),
30 _event_list_selection(_event_list_view->get_selection())
31 {
32 }
33
34 Gtk::TreeView *_event_list_view;
35
36 // Map of connections used to temporary block/unblock callbacks in a TreeView
37 Inkscape::EventLog::CallbackMap *_callback_connections;
38
39 Glib::RefPtr<Gtk::TreeSelection> _event_list_selection;
40};
41
42class ConnectionMatcher
43{
44public:
45 ConnectionMatcher(Gtk::TreeView *view,
47 _view(view),
48 _callbacks(callbacks)
49 {
50 }
51
52 bool operator() (DialogConnection const &dlg)
53 {
54 return (_view == dlg._event_list_view) && (_callbacks == dlg._callback_connections);
55 }
56
57 Gtk::TreeView *_view;
59};
60
61void addBlocker(std::vector<std::unique_ptr<SignalBlocker<sigc::connection>>> &blockers, sigc::connection *connection)
62{
63 blockers.emplace_back(new SignalBlocker(*connection));
64}
65
66
67} // namespace
68
69namespace Inkscape {
70
71class EventLogPrivate
72{
73public:
74 EventLogPrivate() :
75 _connections()
76 {
77 }
78
79 bool isConnected() const
80 {
81 return !_connections.empty();
82 }
83
84 void addDialogConnection(Gtk::TreeView *event_list_view,
85 Inkscape::EventLog::CallbackMap *callback_connections,
86 Glib::RefPtr<Gtk::TreeStore> event_list_store,
88 {
89 if (std::find_if(_connections.begin(), _connections.end(), ConnectionMatcher(event_list_view, callback_connections)) != _connections.end()) {
90 // skipping
91 }
92 else
93 {
94 DialogConnection dlg(event_list_view, callback_connections);
95
96 dlg._event_list_selection->set_mode(Gtk::SelectionMode::SINGLE);
97
98 {
99 std::vector<std::unique_ptr<SignalBlocker<sigc::connection>>> blockers;
100 addBlocker(blockers, &(*dlg._callback_connections)[Inkscape::EventLog::CALLB_SELECTION_CHANGE]);
101 addBlocker(blockers, &(*dlg._callback_connections)[Inkscape::EventLog::CALLB_EXPAND]);
102
103 dlg._event_list_view->expand_to_path(event_list_store->get_path(curr_event));
104 dlg._event_list_selection->select(curr_event);
105 }
106 _connections.push_back(dlg);
107 }
108 }
109
110 void removeDialogConnection(Gtk::TreeView *event_list_view, Inkscape::EventLog::CallbackMap *callback_connections)
111 {
112 std::vector<DialogConnection>::iterator it = std::find_if(_connections.begin(), _connections.end(), ConnectionMatcher(event_list_view, callback_connections));
113 if (it != _connections.end()) {
114 _connections.erase(it);
115 }
116 }
117
118 void collapseRow(Gtk::TreeModel::Path const &path)
119 {
120 std::vector<std::unique_ptr<SignalBlocker<sigc::connection>>> blockers;
121 for (auto & _connection : _connections)
122 {
123 addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_SELECTION_CHANGE]);
124 addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_COLLAPSE]);
125 }
126
127 for (auto & _connection : _connections)
128 {
129 _connection._event_list_view->collapse_row(path);
130 }
131 }
132
133 void selectRow(Gtk::TreeModel::Path const &path)
134 {
135 std::vector<std::unique_ptr<SignalBlocker<sigc::connection>>> blockers;
136 for (auto & _connection : _connections)
137 {
138 addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_SELECTION_CHANGE]);
139 addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_EXPAND]);
140 }
141
142 for (auto & _connection : _connections)
143 {
144 _connection._event_list_view->expand_to_path(path);
145 _connection._event_list_selection->select(path);
146 _connection._event_list_view->scroll_to_row(path);
147 }
148 }
149
150 void clearEventList(Glib::RefPtr<Gtk::TreeStore> eventListStore)
151 {
152 if (eventListStore) {
153 std::vector<std::unique_ptr<SignalBlocker<sigc::connection>>> blockers;
154 for (auto & _connection : _connections)
155 {
156 addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_SELECTION_CHANGE]);
157 addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_EXPAND]);
158 }
159
160 eventListStore->clear();
161 }
162 }
163
164 std::vector<DialogConnection> _connections;
165};
166
168{
169 static const EventModelColumns columns;
170 return columns;
171}
172
175 _priv(new EventLogPrivate()),
176 _document (document),
177 _event_list_store (Gtk::TreeStore::create(getColumns())),
178 _curr_event_parent (nullptr),
179 _notifications_blocked (false)
180{
181 // add initial pseudo event
182 Gtk::TreeRow curr_row = *(_event_list_store->append());
183 _curr_event = _last_saved = _last_event = curr_row.get_iter();
184
185 // Save it so it can be modified
186 _first_event = curr_row.get_iter();
187
188 auto &_columns = getColumns();
189 curr_row[_columns.description] = _("[No more changes]");
190 curr_row[_columns.icon_name] = "document-new";
191 curr_row[_columns.child_count] = 0;
192}
193
195 // avoid crash by clearing entries here (see bug #1071082)
196 _priv->clearEventList(_event_list_store);
197
198 delete _priv;
199 _priv = nullptr;
200}
201
202void
204{
205 if ( !_notifications_blocked ) {
206 auto &_columns = getColumns();
207
208 // make sure the supplied event matches the next undoable event
209 g_return_if_fail ( _getUndoEvent() && (*(_getUndoEvent()))[_columns.event] == log );
210
211 // if we're on the first child event...
212 if ( _curr_event->parent() &&
213 _curr_event == _curr_event->parent()->children().begin() )
214 {
215 // ...back up to the parent
216 _curr_event = _curr_event->parent();
217 _curr_event_parent = (iterator)nullptr;
218
219 } else {
220
221 // if we're about to leave a branch, collapse it
222 if ( !_curr_event->children().empty() ) {
223 _priv->collapseRow(_event_list_store->get_path(_curr_event));
224 }
225
226 --_curr_event;
227
228 // if we're entering a branch, move to the end of it
229 if (!_curr_event->children().empty()) {
231 _curr_event = _curr_event->children().end();
232 --_curr_event;
233 }
234 }
235
237
238 // update the view
239 if (_priv->isConnected()) {
240 Gtk::TreePath curr_path = _event_list_store->get_path(_curr_event);
241 _priv->selectRow(curr_path);
242 }
243
245 }
246
247}
248
249void
251{
252 if ( !_notifications_blocked ) {
253 auto &_columns = getColumns();
254
255 // make sure the supplied event matches the next redoable event
256 g_return_if_fail ( _getRedoEvent() && (*(_getRedoEvent()))[_columns.event] == log );
257
258 // if we're on a parent event...
259 if ( !_curr_event->children().empty() ) {
260
261 // ...move to its first child
263 _curr_event = _curr_event->children().begin();
264
265 } else {
266
267 ++_curr_event;
268
269 // if we are about to leave a branch...
270 if ( _curr_event->parent() &&
271 _curr_event == _curr_event->parent()->children().end() )
272 {
273
274 // ...collapse it
275 _priv->collapseRow(_event_list_store->get_path(_curr_event->parent()));
276
277 // ...and move to the next event at parent level
278 _curr_event = _curr_event->parent();
279 _curr_event_parent = (iterator)nullptr;
280
281 ++_curr_event;
282 }
283 }
284
286
287 // update the view
288 if (_priv->isConnected()) {
289 Gtk::TreePath curr_path = _event_list_store->get_path(_curr_event);
290 _priv->selectRow(curr_path);
291 }
292
294 }
295
296}
297
298void
300{
301 _clearRedo();
302
303 auto icon_name = log->icon_name;
304
305 Gtk::TreeRow curr_row;
306 auto &_columns = getColumns();
307
308 // if the new event is of the same type as the previous then create a new branch
309 if ( icon_name == Glib::ustring{(*_curr_event)[_columns.icon_name]} ) {
310 if ( !_curr_event_parent ) {
312 }
313 curr_row = *(_event_list_store->append(_curr_event_parent->children()));
314 (*_curr_event_parent)[_columns.child_count] = _curr_event_parent->children().size() + 1;
315 } else {
316 curr_row = *(_event_list_store->append());
317 curr_row[_columns.child_count] = 1;
318
319 _curr_event = _last_event = curr_row.get_iter();
320
321 // collapse if we're leaving a branch
322 if (_curr_event_parent) {
323 _priv->collapseRow(_event_list_store->get_path(_curr_event_parent));
324 }
325
326 _curr_event_parent = (iterator)(nullptr);
327 }
328
329 _curr_event = _last_event = curr_row.get_iter();
330
331 curr_row[_columns.event] = log;
332 curr_row[_columns.icon_name] = icon_name;
333 curr_row[_columns.description] = log->description;
334
336
337 // update the view
338 if (_priv->isConnected()) {
339 Gtk::TreePath curr_path = _event_list_store->get_path(_curr_event);
340 _priv->selectRow(curr_path);
341 }
342
344}
345
346void
348{
349 auto &columns = getColumns();
350
351 if (_event_list_store->children().size() == 1)
352 return; // Nothing to do, nothing in the undo log
353
354 // We only have to look at one item because we never expire from the middle.
355 iterator iter = _event_list_store->children().begin();
356
357 // Skip first item, it's the non-event label
358 if (iter == _first_event)
359 iter++;
360
361 assert((*iter)[columns.event] == log);
362
363 iterator to_remove;
364 if (iter->children().size() > 0) {
365 // Move first child's log to parent as the parent is being deleted
366 to_remove = iter->children().begin();
367
368 Event *child_log = (*to_remove)[columns.event];
369 Glib::ustring desc = (*to_remove)[columns.description];
370 (*iter)[columns.event] = child_log;
371 (*iter)[columns.description] = desc;
372 } else {
373 to_remove = iter;
374 }
375
376 // This should never happen as we should never expire undo items from the middle.
377 assert(to_remove->children().size() == 0);
378
379 if (auto parent = to_remove->parent()) {
380 (*parent)[columns.child_count] = to_remove->parent()->children().size() - 1;
381 }
382
383 _event_list_store->erase(to_remove);
384
385 // Tell the user about the forgotten undo stack
386 if ((*_first_event)[columns.child_count] == 0) {
387 (*_first_event)[columns.description] = _("[Changes forgotten]");
388 }
389 (*_first_event)[columns.child_count] = (*_first_event)[columns.child_count] + 1;
390}
391
392void
398
399void
405
406void EventLog::addDialogConnection(Gtk::TreeView *event_list_view, CallbackMap *callback_connections)
407{
408 _priv->addDialogConnection(event_list_view, callback_connections, _event_list_store, _curr_event);
409}
410
411void EventLog::removeDialogConnection(Gtk::TreeView *event_list_view, CallbackMap *callback_connections)
412{
413 _priv->removeDialogConnection(event_list_view, callback_connections);
414}
415
416// Enable/disable undo/redo GUI items.
417void
419{
420 if (_document) {
421 enable_undo_actions(_document, static_cast<bool>(_getUndoEvent()), static_cast<bool>(_getRedoEvent()));
422 }
423}
424
425
428{
429 const_iterator undo_event = (const_iterator)nullptr;
430 if( _curr_event != _event_list_store->children().begin() )
431 undo_event = _curr_event;
432 return undo_event;
433}
434
437{
438 const_iterator redo_event = (const_iterator)nullptr;
439
440 if ( _curr_event != _last_event ) {
441
442 if ( !_curr_event->children().empty() )
443 redo_event = _curr_event->children().begin();
444 else {
445 redo_event = _curr_event;
446 ++redo_event;
447
448 if ( redo_event->parent() &&
449 redo_event == redo_event->parent()->children().end() ) {
450
451 redo_event = redo_event->parent();
452 ++redo_event;
453
454 }
455 }
456
457 }
458
459 return redo_event;
460}
461
462void
464{
465 // TODO: Implement when needed
466}
467
468void
470{
471 if ( _last_event != _curr_event ) {
472 auto &_columns = getColumns();
473
475
476 if ( !_last_event->children().empty() ) {
477 _last_event = _last_event->children().begin();
478 } else {
479 ++_last_event;
480 }
481
482 while ( _last_event != _event_list_store->children().end() ) {
483
484 if (_last_event->parent()) {
485 while ( _last_event != _last_event->parent()->children().end() ) {
487 }
488 _last_event = _last_event->parent();
489
490 (*_last_event)[_columns.child_count] = _last_event->children().size() + 1;
491
492 ++_last_event;
493 } else {
495 }
496
497 }
498
499 }
500}
501
502/* mark document as untouched if we reach a state where the document was previously saved */
503void
505 g_return_if_fail (_document);
506 if (_curr_event == _last_saved) {
508 }
509}
510
511} // namespace Inkscape
512
513
514/*
515 Local Variables:
516 mode:c++
517 c-file-style:"stroustrup"
518 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
519 indent-tabs-mode:nil
520 fill-column:99
521 End:
522*/
523// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
void enable_undo_actions(SPDocument *document, bool undo, bool redo)
Actions for Undo/Redo tied to document.
void addDialogConnection(Gtk::TreeView *event_list_view, CallbackMap *callback_connections)
Connect with a TreeView.
SPDocument * _document
Definition event-log.h:122
Glib::RefPtr< Gtk::TreeStore > _event_list_store
Definition event-log.h:124
void removeDialogConnection(Gtk::TreeView *event_list_view, CallbackMap *callback_connections)
Disconnect from a TreeView.
iterator _curr_event
Definition event-log.h:127
EventLog(SPDocument *document)
void notifyUndoExpired(Event *log) override
Triggered when undo items are removed from the back of the log (expired)
Gtk::TreeModel::const_iterator const_iterator
Definition event-log.h:48
void notifyClearRedoEvent() override
Triggered when the redo log is cleared.
void notifyUndoCommitEvent(Event *log) override
Triggered when a set of transactions is committed to the undo log.
Gtk::TreeModel::iterator iterator
Definition event-log.h:47
iterator _last_event
Definition event-log.h:128
EventLogPrivate * _priv
Definition event-log.h:120
void notifyClearUndoEvent() override
Triggered when the undo log is cleared.
~EventLog() override
void notifyUndoEvent(Event *log) override
Modifies the log's entries and the view's selection when triggered.
void notifyRedoEvent(Event *log) override
Triggered when the user issues a redo command.
std::map< const CallbackTypes, sigc::connection > CallbackMap
Definition event-log.h:102
const_iterator _getUndoEvent() const
iterator _last_saved
Definition event-log.h:131
const_iterator _getRedoEvent() const
bool _notifications_blocked
Definition event-log.h:133
iterator _curr_event_parent
Definition event-log.h:129
iterator _first_event
Definition event-log.h:126
static const EventModelColumns & getColumns()
Observes changes made to the undo and redo stacks.
Typed SVG document implementation.
Definition document.h:101
void setModifiedSinceSave(bool const modified=true)
Indicate to the user if the document has been modified since the last save by displaying a "*" in fro...
RAII blocker for sigc++ signals.
static char const *const parent
Definition dir-util.cpp:70
Definition desktop.h:50
Helper class to stream background task notifications as a series of messages.
Piecewise< SBasis > log(Interval in)
Definition pw-funcs.cpp:37
std::unique_ptr< Toolbar >(* create)()
Definition toolbars.cpp:56