// SPDX-License-Identifier: GPL-2.0-or-later /* * Author: * Gustav Broberg * Jon A. Cruz * * Copyright (c) 2014 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "event-log.h" #include #include "desktop.h" #include "document.h" #include "inkscape.h" #include "inkscape-application.h" #include "inkscape-window.h" #include "ui/desktop/menubar.h" #include "util/signal-blocker.h" namespace { class DialogConnection { public: DialogConnection(Gtk::TreeView *event_list_view, Inkscape::EventLog::CallbackMap *callback_connections) : _event_list_view(event_list_view), _callback_connections(callback_connections), _event_list_selection(_event_list_view->get_selection()) { } Gtk::TreeView *_event_list_view; // Map of connections used to temporary block/unblock callbacks in a TreeView Inkscape::EventLog::CallbackMap *_callback_connections; Glib::RefPtr _event_list_selection; /// @todo remove this and use _event_list_view's call }; class ConnectionMatcher { public: ConnectionMatcher(Gtk::TreeView *view, Inkscape::EventLog::CallbackMap *callbacks) : _view(view), _callbacks(callbacks) { } bool operator() (DialogConnection const &dlg) { return (_view == dlg._event_list_view) && (_callbacks == dlg._callback_connections); } Gtk::TreeView *_view; Inkscape::EventLog::CallbackMap *_callbacks; }; void addBlocker(std::vector > &blockers, sigc::connection *connection) { blockers.emplace_back(new SignalBlocker(connection)); } } // namespace namespace Inkscape { class EventLogPrivate { public: EventLogPrivate() : _connections() { } bool isConnected() const { return !_connections.empty(); } void addDialogConnection(Gtk::TreeView *event_list_view, Inkscape::EventLog::CallbackMap *callback_connections, Glib::RefPtr event_list_store, Inkscape::EventLog::iterator &curr_event) { if (std::find_if(_connections.begin(), _connections.end(), ConnectionMatcher(event_list_view, callback_connections)) != _connections.end()) { // skipping } else { DialogConnection dlg(event_list_view, callback_connections); dlg._event_list_selection->set_mode(Gtk::SELECTION_SINGLE); { std::vector > blockers; addBlocker(blockers, &(*dlg._callback_connections)[Inkscape::EventLog::CALLB_SELECTION_CHANGE]); addBlocker(blockers, &(*dlg._callback_connections)[Inkscape::EventLog::CALLB_EXPAND]); dlg._event_list_view->expand_to_path(event_list_store->get_path(curr_event)); dlg._event_list_selection->select(curr_event); } _connections.push_back(dlg); } } void removeDialogConnection(Gtk::TreeView *event_list_view, Inkscape::EventLog::CallbackMap *callback_connections) { std::vector::iterator it = std::find_if(_connections.begin(), _connections.end(), ConnectionMatcher(event_list_view, callback_connections)); if (it != _connections.end()) { _connections.erase(it); } } void collapseRow(Gtk::TreeModel::Path const &path) { std::vector > blockers; for (auto & _connection : _connections) { addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_SELECTION_CHANGE]); addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_COLLAPSE]); } for (auto & _connection : _connections) { _connection._event_list_view->collapse_row(path); } } void selectRow(Gtk::TreeModel::Path const &path) { std::vector > blockers; for (auto & _connection : _connections) { addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_SELECTION_CHANGE]); addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_EXPAND]); } for (auto & _connection : _connections) { _connection._event_list_view->expand_to_path(path); _connection._event_list_selection->select(path); _connection._event_list_view->scroll_to_row(path); } } void clearEventList(Glib::RefPtr eventListStore) { if (eventListStore) { std::vector > blockers; for (auto & _connection : _connections) { addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_SELECTION_CHANGE]); addBlocker(blockers, &(*_connection._callback_connections)[Inkscape::EventLog::CALLB_EXPAND]); } eventListStore->clear(); } } std::vector _connections; }; const EventLog::EventModelColumns &EventLog::getColumns() { static const EventModelColumns columns; return columns; } EventLog::EventLog(SPDocument* document) : UndoStackObserver(), _priv(new EventLogPrivate()), _document (document), _event_list_store (Gtk::TreeStore::create(getColumns())), _curr_event_parent (nullptr), _notifications_blocked (false) { // add initial pseudo event Gtk::TreeRow curr_row = *(_event_list_store->append()); _curr_event = _last_saved = _last_event = curr_row; auto &_columns = getColumns(); curr_row[_columns.description] = _("[Unchanged]"); curr_row[_columns.icon_name] = "document-new"; } EventLog::~EventLog() { // avoid crash by clearing entries here (see bug #1071082) _priv->clearEventList(_event_list_store); delete _priv; _priv = nullptr; } void EventLog::notifyUndoEvent(Event* log) { if ( !_notifications_blocked ) { auto &_columns = getColumns(); // make sure the supplied event matches the next undoable event g_return_if_fail ( _getUndoEvent() && (*(_getUndoEvent()))[_columns.event] == log ); // if we're on the first child event... if ( _curr_event->parent() && _curr_event == _curr_event->parent()->children().begin() ) { // ...back up to the parent _curr_event = _curr_event->parent(); _curr_event_parent = (iterator)nullptr; } else { // if we're about to leave a branch, collapse it if ( !_curr_event->children().empty() ) { _priv->collapseRow(_event_list_store->get_path(_curr_event)); } --_curr_event; // if we're entering a branch, move to the end of it if (!_curr_event->children().empty()) { _curr_event_parent = _curr_event; _curr_event = _curr_event->children().end(); --_curr_event; } } checkForVirginity(); // update the view if (_priv->isConnected()) { Gtk::TreePath curr_path = _event_list_store->get_path(_curr_event); _priv->selectRow(curr_path); } updateUndoVerbs(); } } void EventLog::notifyRedoEvent(Event* log) { if ( !_notifications_blocked ) { auto &_columns = getColumns(); // make sure the supplied event matches the next redoable event g_return_if_fail ( _getRedoEvent() && (*(_getRedoEvent()))[_columns.event] == log ); // if we're on a parent event... if ( !_curr_event->children().empty() ) { // ...move to its first child _curr_event_parent = _curr_event; _curr_event = _curr_event->children().begin(); } else { ++_curr_event; // if we are about to leave a branch... if ( _curr_event->parent() && _curr_event == _curr_event->parent()->children().end() ) { // ...collapse it _priv->collapseRow(_event_list_store->get_path(_curr_event->parent())); // ...and move to the next event at parent level _curr_event = _curr_event->parent(); _curr_event_parent = (iterator)nullptr; ++_curr_event; } } checkForVirginity(); // update the view if (_priv->isConnected()) { Gtk::TreePath curr_path = _event_list_store->get_path(_curr_event); _priv->selectRow(curr_path); } updateUndoVerbs(); } } void EventLog::notifyUndoCommitEvent(Event* log) { _clearRedo(); auto icon_name = log->icon_name; Gtk::TreeRow curr_row; auto &_columns = getColumns(); // if the new event is of the same type as the previous then create a new branch if ( icon_name == (*_curr_event)[_columns.icon_name] ) { if ( !_curr_event_parent ) { _curr_event_parent = _curr_event; } curr_row = *(_event_list_store->append(_curr_event_parent->children())); (*_curr_event_parent)[_columns.child_count] = _curr_event_parent->children().size() + 1; } else { curr_row = *(_event_list_store->append()); curr_row[_columns.child_count] = 1; _curr_event = _last_event = curr_row; // collapse if we're leaving a branch if (_curr_event_parent) { _priv->collapseRow(_event_list_store->get_path(_curr_event_parent)); } _curr_event_parent = (iterator)(nullptr); } _curr_event = _last_event = curr_row; curr_row[_columns.event] = log; curr_row[_columns.icon_name] = icon_name; curr_row[_columns.description] = log->description; checkForVirginity(); // update the view if (_priv->isConnected()) { Gtk::TreePath curr_path = _event_list_store->get_path(_curr_event); _priv->selectRow(curr_path); } updateUndoVerbs(); } void EventLog::notifyClearUndoEvent() { _clearUndo(); updateUndoVerbs(); } void EventLog::notifyClearRedoEvent() { _clearRedo(); updateUndoVerbs(); } void EventLog::addDialogConnection(Gtk::TreeView *event_list_view, CallbackMap *callback_connections) { _priv->addDialogConnection(event_list_view, callback_connections, _event_list_store, _curr_event); } void EventLog::removeDialogConnection(Gtk::TreeView *event_list_view, CallbackMap *callback_connections) { _priv->removeDialogConnection(event_list_view, callback_connections); } // Enable/disable undo/redo GUI items. void EventLog::updateUndoVerbs() { auto updateActions = [=](Gio::ActionMap *actions) { auto undo_action = actions->lookup_action("undo"); auto redo_action = actions->lookup_action("redo"); auto undo_saction = Glib::RefPtr::cast_dynamic(undo_action); auto redo_saction = Glib::RefPtr::cast_dynamic(redo_action); // GTK4 // auto undo_saction = dynamic_cast(undo_action); // auto redo_saction = dynamic_cast(redo_action); if (!undo_saction || !redo_saction) { std::cerr << "EventLog::updateUndoVerbs: can't find undo or redo action!" << std::endl; return; } // Enable/disable menu items. undo_saction->set_enabled(static_cast(_getUndoEvent())); redo_saction->set_enabled(static_cast(_getRedoEvent())); }; if (_document) { updateActions(_document->getActionGroup().operator->()); } if (auto *app = InkscapeApplication::instance()) { updateActions(app->gio_app()); } } EventLog::const_iterator EventLog::_getUndoEvent() const { const_iterator undo_event = (const_iterator)nullptr; if( _curr_event != _event_list_store->children().begin() ) undo_event = _curr_event; return undo_event; } EventLog::const_iterator EventLog::_getRedoEvent() const { const_iterator redo_event = (const_iterator)nullptr; if ( _curr_event != _last_event ) { if ( !_curr_event->children().empty() ) redo_event = _curr_event->children().begin(); else { redo_event = _curr_event; ++redo_event; if ( redo_event->parent() && redo_event == redo_event->parent()->children().end() ) { redo_event = redo_event->parent(); ++redo_event; } } } return redo_event; } void EventLog::_clearUndo() { // TODO: Implement when needed } void EventLog::_clearRedo() { if ( _last_event != _curr_event ) { auto &_columns = getColumns(); _last_event = _curr_event; if ( !_last_event->children().empty() ) { _last_event = _last_event->children().begin(); } else { ++_last_event; } while ( _last_event != _event_list_store->children().end() ) { if (_last_event->parent()) { while ( _last_event != _last_event->parent()->children().end() ) { _last_event = _event_list_store->erase(_last_event); } _last_event = _last_event->parent(); (*_last_event)[_columns.child_count] = _last_event->children().size() + 1; ++_last_event; } else { _last_event = _event_list_store->erase(_last_event); } } } } /* mark document as untouched if we reach a state where the document was previously saved */ void EventLog::checkForVirginity() { g_return_if_fail (_document); if (_curr_event == _last_saved) { _document->setModifiedSinceSave(false); } } } // namespace Inkscape /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :