[pitivi] project_: Keep the assets undoable actions as the user expects
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] project_: Keep the assets undoable actions as the user expects
- Date: Wed, 4 Jan 2017 15:45:35 +0000 (UTC)
commit 675e06983bde50670a31604abaf4133b4ae263ba
Author: Alexandru Băluț <alexandru balut gmail com>
Date: Tue Dec 20 01:05:28 2016 +0100
project_: Keep the assets undoable actions as the user expects
We were starting an undoable-action-log transaction when a new asset was
added to the project. This was very ugly because by the time that
transaction was closed, the user was able to make other operations on
the timeline and those were included in the initial assets-added
transaction.
This got worse with the proxy functionality since that transaction can
now take a very long time until complete, which means the undo/redo
stack was easily messed up.
Taking advantage of the fact that it does not really matter when the
assets are actually added to the project, we now record the intention of
adding them, or the intention of proxying them. Then when they are ready
to be used, the user can use them and all should be fine.
Fixes https://phabricator.freedesktop.org/T7531
Reviewed-by: Thibault Saunier <tsaunier gnome org>
Differential Revision: https://phabricator.freedesktop.org/D1575
pitivi/project.py | 30 ++++++++++---------
pitivi/undo/project.py | 70 +++++++++++++++++++++++++++++++++++++++----
pitivi/undo/timeline.py | 6 ++--
pitivi/undo/undo.py | 43 +++++++++++++++------------
tests/test_undo.py | 8 ++--
tests/test_undo_project.py | 38 ++++++++++++++----------
6 files changed, 132 insertions(+), 63 deletions(-)
---
diff --git a/pitivi/project.py b/pitivi/project.py
index 24b63c0..7123102 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -36,6 +36,8 @@ from pitivi.configure import get_ui_dir
from pitivi.preset import AudioPresetManager
from pitivi.preset import VideoPresetManager
from pitivi.render import Encoders
+from pitivi.undo.project import AssetAddedIntention
+from pitivi.undo.project import AssetProxiedIntention
from pitivi.utils.loggable import Loggable
from pitivi.utils.misc import isWritable
from pitivi.utils.misc import path_from_uri
@@ -986,7 +988,6 @@ class Project(Loggable, GES.Project):
def __updateAssetLoadingProgress(self, estimated_time=0):
if not self.loading_assets:
- self.app.action_log.commit("Adding assets")
self.emit("asset-loading-progress", 100, estimated_time)
return
@@ -1181,16 +1182,17 @@ class Project(Loggable, GES.Project):
def use_proxies_for_assets(self, assets):
originals = []
for asset in assets:
- proxy_target = asset.get_proxy_target()
- if not proxy_target:
+ if not asset.get_proxy_target():
# The asset is not a proxy.
originals.append(asset)
if originals:
- self.app.action_log.begin("Adding assets")
- for asset in originals:
- self._prepare_asset_processing(asset)
- asset.force_proxying = True
- self.app.proxy_manager.add_job(asset)
+ with self.app.action_log.started("Proxying assets"):
+ for asset in originals:
+ action = AssetProxiedIntention(asset, self, self.app.proxy_manager)
+ self.app.action_log.push(action)
+ self._prepare_asset_processing(asset)
+ asset.force_proxying = True
+ self.app.proxy_manager.add_job(asset)
def disable_proxies_for_assets(self, assets, delete_proxy_file=False):
for asset in assets:
@@ -1211,9 +1213,6 @@ class Project(Loggable, GES.Project):
# The asset is an original which is not being proxied.
self.app.proxy_manager.cancel_job(asset)
- if assets:
- self.setModificationState(True)
-
def hasDefaultName(self):
return DEFAULT_NAME == self.name
@@ -1272,9 +1271,12 @@ class Project(Loggable, GES.Project):
Args:
uris (List[str]): The URIs of the assets.
"""
- self.app.action_log.begin("Adding assets")
- for uri in uris:
- self.create_asset(quote_uri(uri), GES.UriClip)
+ with self.app.action_log.started("Adding assets"):
+ for uri in uris:
+ if self.create_asset(quote_uri(uri), GES.UriClip):
+ # The asset was not already part of the project.
+ action = AssetAddedIntention(self, uri)
+ self.app.action_log.push(action)
def assetsForUris(self, uris):
assets = []
diff --git a/pitivi/undo/project.py b/pitivi/undo/project.py
index 6296e04..8c45e19 100644
--- a/pitivi/undo/project.py
+++ b/pitivi/undo/project.py
@@ -21,22 +21,50 @@ from gi.repository import GObject
from gi.repository import Gst
from pitivi.undo.timeline import TimelineObserver
+from pitivi.undo.undo import Action
from pitivi.undo.undo import MetaContainerObserver
from pitivi.undo.undo import UndoableAction
-class AssetAddedAction(UndoableAction):
+class AssetAddedIntention(UndoableAction):
+ """The intention of adding an asset to a project.
- def __init__(self, project, asset):
+ This should be created when the async operation starts.
+ See also AssetAddedAction.
+ """
+
+ def __init__(self, project, uri):
UndoableAction.__init__(self)
self.project = project
- self.asset = asset
+ self.uri = uri
+ self.asset = None
+ self.project.connect("asset-added", self._asset_added_cb)
+
+ def _asset_added_cb(self, project, asset):
+ if asset.get_id() == self.uri:
+ self.asset = asset
+ self.project.disconnect_by_func(self._asset_added_cb)
def undo(self):
- self.project.remove_asset(self.asset)
+ # The asset might be missing if removed before it's added
+ if self.asset:
+ self.project.remove_asset(self.asset)
def do(self):
- self.project.add_asset(self.asset)
+ if self.asset:
+ self.project.add_asset(self.asset)
+
+
+class AssetAddedAction(Action):
+ """The adding of an asset to a project.
+
+ This should be created when the asset has been added.
+ See also AssetAddedIntention.
+ """
+
+ def __init__(self, asset):
+ Action.__init__(self)
+ self.asset = asset
def asScenarioAction(self):
st = Gst.Structure.new_empty("add-asset")
@@ -47,6 +75,7 @@ class AssetAddedAction(UndoableAction):
class AssetRemovedAction(UndoableAction):
+ """The removal of an asset from a project."""
def __init__(self, project, asset):
UndoableAction.__init__(self)
@@ -67,6 +96,33 @@ class AssetRemovedAction(UndoableAction):
return st
+class AssetProxiedIntention(UndoableAction):
+ """The intention of proxying an asset.
+
+ Attributes:
+ asset (GES.Asset): The original asset to be proxied.
+ proxy_manager (pitivi.utils.proxy.ProxyManager): The manager
+ controlling the proxy generation.
+ """
+
+ def __init__(self, asset, project, proxy_manager):
+ UndoableAction.__init__(self)
+ self.asset = asset
+ self.project = project
+ self.proxy_manager = proxy_manager
+
+ def do(self):
+ self.asset.force_proxying = True
+ self.proxy_manager.add_job(self.asset)
+
+ def undo(self):
+ self.proxy_manager.cancel_job(self.asset)
+ proxy = self.asset.props.proxy
+ self.asset.set_proxy(None)
+ if proxy:
+ self.project.remove_asset(proxy)
+
+
class ProjectObserver(MetaContainerObserver):
"""Monitors a project instance and reports UndoableActions.
@@ -81,10 +137,10 @@ class ProjectObserver(MetaContainerObserver):
self.timeline_observer = TimelineObserver(project.ges_timeline,
action_log)
- def _assetAddedCb(self, project, asset):
+ def _assetAddedCb(self, unused_project, asset):
if not isinstance(asset, GES.UriClipAsset):
return
- action = AssetAddedAction(project, asset)
+ action = AssetAddedAction(asset)
self.action_log.push(action)
def _assetRemovedCb(self, project, asset):
diff --git a/pitivi/undo/timeline.py b/pitivi/undo/timeline.py
index fc317cc..c6823ca 100644
--- a/pitivi/undo/timeline.py
+++ b/pitivi/undo/timeline.py
@@ -21,11 +21,11 @@ from gi.repository import GObject
from gi.repository import Gst
from pitivi.effects import PROPS_TO_IGNORE
+from pitivi.undo.undo import Action
from pitivi.undo.undo import ExpandableUndoableAction
from pitivi.undo.undo import FinalizingAction
from pitivi.undo.undo import GObjectObserver
from pitivi.undo.undo import MetaContainerObserver
-from pitivi.undo.undo import SimpleUndoableAction
from pitivi.undo.undo import UndoableAction
from pitivi.undo.undo import UndoableAutomaticObjectAction
from pitivi.utils.loggable import Loggable
@@ -543,10 +543,10 @@ class KeyframeChangedAction(UndoableAction):
self.control_source.set(time, value)
-class ControlSourceSetAction(SimpleUndoableAction):
+class ControlSourceSetAction(Action):
def __init__(self, action_info):
- SimpleUndoableAction.__init__(self)
+ Action.__init__(self)
self.action_info = action_info
def asScenarioAction(self):
diff --git a/pitivi/undo/undo.py b/pitivi/undo/undo.py
index c5dcef2..5483154 100644
--- a/pitivi/undo/undo.py
+++ b/pitivi/undo/undo.py
@@ -34,26 +34,30 @@ class UndoWrongStateError(UndoError):
pass
-class UndoableAction(GObject.Object, Loggable):
+class Action(GObject.Object, Loggable):
+ """Something which might worth logging in a scenario."""
+
+ def __init__(self):
+ GObject.Object.__init__(self)
+ Loggable.__init__(self)
+
+ def asScenarioAction(self):
+ raise NotImplementedError()
+
+
+class UndoableAction(Action):
"""An action that can be undone.
When your object's state changes, create an UndoableAction to allow
reverting the change later on.
"""
- def __init__(self):
- GObject.Object.__init__(self)
- Loggable.__init__(self)
-
def do(self):
raise NotImplementedError()
def undo(self):
raise NotImplementedError()
- def asScenarioAction(self):
- raise NotImplementedError()
-
class UndoableAutomaticObjectAction(UndoableAction):
"""An action on an automatically created object.
@@ -90,7 +94,7 @@ class UndoableAutomaticObjectAction(UndoableAction):
cls.__updates[other] = new_auto_object
-class ExpandableUndoableAction(GObject.Object, Loggable):
+class ExpandableUndoableAction(UndoableAction):
"""An action which can include immediately following actions."""
def expand(self, action):
@@ -106,15 +110,6 @@ class ExpandableUndoableAction(GObject.Object, Loggable):
raise NotImplementedError()
-class SimpleUndoableAction(UndoableAction):
-
- def do(self):
- pass
-
- def undo(self):
- pass
-
-
class FinalizingAction:
"""Base class for actions applied when an undo or redo is performed."""
@@ -217,9 +212,19 @@ class UndoableActionLog(GObject.Object, Loggable):
self.emit("begin", stack)
def push(self, action):
- """Adds an action to the current operation."""
+ """Reports a change.
+
+ Args:
+ action (Action): The action representing the change.
+ If it's an UndoableAction, it's added to the current
+ operation, if any.
+ """
self.emit("pre-push", action)
+ if not isinstance(action, UndoableAction):
+ # Nothing else to do with it.
+ return
+
if self.running:
self.debug("Ignore push because running: %s", action)
return
diff --git a/tests/test_undo.py b/tests/test_undo.py
index 68a4ed9..c2d82d9 100644
--- a/tests/test_undo.py
+++ b/tests/test_undo.py
@@ -148,7 +148,7 @@ class TestUndoableActionLog(TestCase):
self.assertTrue(self.log.is_in_transaction())
self.assertEqual(self.log.undo_stacks, [])
- self.log.push(mock.Mock())
+ self.log.push(mock.Mock(spec=UndoableAction))
self.log.commit("meh")
self.assertEqual(len(self.signals), 3)
name, (stack, action) = self.signals[1]
@@ -184,7 +184,7 @@ class TestUndoableActionLog(TestCase):
self.assertTrue(self.log.is_in_transaction())
self.assertEqual(self.log.undo_stacks, [])
- self.log.push(mock.Mock())
+ self.log.push(mock.Mock(spec=UndoableAction))
self.log.commit("nested")
self.assertEqual(len(self.signals), 4)
name, (stack, action) = self.signals[2]
@@ -208,9 +208,9 @@ class TestUndoableActionLog(TestCase):
action1 = mock.Mock()
action2 = mock.Mock()
with self.log.started("one", finalizing_action=action1):
- self.log.push(mock.Mock())
+ self.log.push(mock.Mock(spec=UndoableAction))
with self.log.started("two", finalizing_action=action2):
- self.log.push(mock.Mock())
+ self.log.push(mock.Mock(spec=UndoableAction))
action1.do.assert_called_once_with()
# For now, we call the finalizing action only for the top stack.
action2.do.assert_not_called()
diff --git a/tests/test_undo_project.py b/tests/test_undo_project.py
index a556e1b..9141dc7 100644
--- a/tests/test_undo_project.py
+++ b/tests/test_undo_project.py
@@ -48,20 +48,20 @@ class TestProjectUndo(TestCase):
self.assertFalse(self.action_log.undo_stacks)
def test_asset_added(self):
+ uris = [common.get_sample_uri("tears_of_steel.webm")]
mainloop = common.create_main_loop()
- def commit_cb(unused_action_log, stack):
- self.assertEqual(stack.action_group_name, "Adding assets")
- mainloop.quit()
-
- self.action_log.connect("commit", commit_cb)
-
def loaded_cb(unused_project, unused_timeline):
- uris = [common.get_sample_uri("tears_of_steel.webm")]
self.project.addUris(uris)
self.project.connect_after("loaded", loaded_cb)
+ def progress_cb(unused_project, progress, unused_estimated_time):
+ if progress == 100:
+ mainloop.quit()
+
+ self.project.connect_after("asset-loading-progress", progress_cb)
+
mainloop.run()
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 1)
@@ -75,17 +75,19 @@ class TestProjectUndo(TestCase):
uris = [common.get_sample_uri("tears_of_steel.webm")]
mainloop = common.create_main_loop()
- def commit_cb(unused_action_log, stack):
- self.assertEqual(stack.action_group_name, "Adding assets")
- mainloop.quit()
- self.action_log.connect("commit", commit_cb)
-
def loaded_cb(unused_project, unused_timeline):
+ # The new project has been loaded, add some assets.
self.project.addUris(uris)
self.project.connect_after("loaded", loaded_cb)
+ def progress_cb(unused_project, progress, unused_estimated_time):
+ if progress == 100:
+ # The assets have been loaded.
+ mainloop.quit()
+ self.project.connect_after("asset-loading-progress", progress_cb)
+
mainloop.run()
- self.action_log.disconnect_by_func(commit_cb)
+ self.project.disconnect_by_func(progress_cb)
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 1)
# Make sure the asset is not a proxy.
@@ -101,18 +103,22 @@ class TestProjectUndo(TestCase):
self.app.proxy_manager.connect("error-preparing-asset", error_cb)
def proxy_ready_cb(proxy_manager, asset, proxy):
- uris.remove(asset.props.id)
- if not uris:
- mainloop.quit()
+ mainloop.quit()
self.app.proxy_manager.connect("proxy-ready", proxy_ready_cb)
self.project.use_proxies_for_assets(assets)
mainloop.run()
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 2)
+
+ # Undo proxying.
self.action_log.undo()
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 1)
+
+ # Redo proxying.
self.action_log.redo()
+ # Wait for the proxy to be signalled as ready.
+ mainloop.run()
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 2)
def test_project_settings(self):
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]