class TestUndoableActionLog(TestCase): def setUp(self): self.log = UndoableActionLog() self._connectToUndoableActionLog(self.log) self.signals = [] def tearDown(self): self._disconnectFromUndoableActionLog(self.log) def _undoActionLogSignalCb(self, log, *args): args = list(args) signalName = args.pop(-1) self.signals.append((signalName, args)) def _connectToUndoableActionLog(self, log): for signalName in ("begin", "push", "rollback", "commit", "undo", "redo"): log.connect(signalName, self._undoActionLogSignalCb, signalName) def _disconnectFromUndoableActionLog(self, log): self.log.disconnect_by_func(self._undoActionLogSignalCb) def testRollbackWrongState(self): self.assertRaises(UndoWrongStateError, self.log.rollback) def testCommitWrongState(self): self.assertRaises(UndoWrongStateError, self.log.commit) def testPushWrongState(self): # no error in this case self.log.push(None) def testUndoWrongState(self): self.assertRaises(UndoWrongStateError, self.log.undo) def testRedoWrongState(self): self.assertRaises(UndoWrongStateError, self.log.redo) def testCheckpoint(self): self.log.begin("meh") self.log.push(DummyUndoableAction()) self.assertRaises(UndoWrongStateError, self.log.checkpoint) self.log.rollback() self.log.checkpoint() self.assertNotEqual(self.log._checkpoint, None) def testDirty(self): self.assertFalse(self.log.dirty()) self.log.begin("meh") self.log.push(DummyUndoableAction()) self.log.commit() self.assertTrue(self.log.dirty()) self.log.checkpoint() self.assertFalse(self.log.dirty()) self.log.undo() self.assertTrue(self.log.dirty()) self.log.redo() self.assertFalse(self.log.dirty()) def testCommit(self): """ Commit a stack. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, nested) = self.signals[0] self.assertEqual(name, "begin") self.assertFalse(nested) self.assertEqual(self.log.undo_stacks, []) self.log.commit() self.assertEqual(len(self.signals), 2) name, (stack, nested) = self.signals[1] self.assertEqual(name, "commit") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) def testNestedCommit(self): """ Do two nested commits. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, nested) = self.signals[0] self.assertEqual(name, "begin") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("nested") self.assertEqual(len(self.signals), 2) name, (stack, nested) = self.signals[1] self.assertEqual(name, "begin") self.assertTrue(nested) self.assertEqual(self.log.undo_stacks, []) self.log.commit() self.assertEqual(len(self.signals), 3) name, (stack, nested) = self.signals[2] self.assertEqual(name, "commit") self.assertTrue(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(self.log.undo_stacks, []) self.log.commit() self.assertEqual(len(self.signals), 4) name, (stack, nested) = self.signals[3] self.assertEqual(name, "commit") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) def testRollback(self): """ Test a rollback. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, nested) = self.signals[0] self.assertEqual(name, "begin") self.assertFalse(nested) self.log.rollback() self.assertEqual(len(self.signals), 2) name, (stack, nested) = self.signals[1] self.assertEqual(name, "rollback") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def testNestedRollback(self): """ Test two nested rollbacks. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, nested) = self.signals[0] self.assertEqual(name, "begin") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("nested") self.assertEqual(len(self.signals), 2) name, (stack, nested) = self.signals[1] self.assertEqual(name, "begin") self.assertTrue(nested) self.log.rollback() self.assertEqual(len(self.signals), 3) name, (stack, nested) = self.signals[2] self.assertEqual(name, "rollback") self.assertTrue(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.rollback() self.assertEqual(len(self.signals), 4) name, (stack, nested) = self.signals[3] self.assertEqual(name, "rollback") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def testUndoRedo(self): """ Try an undo() redo() sequence. """ # begin self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, nested) = self.signals[0] self.assertEqual(name, "begin") self.assertFalse(nested) # push two actions action1 = DummyUndoableAction() self.log.push(action1) self.assertEqual(len(self.signals), 2) name, (stack, signalAction) = self.signals[1] self.assertEqual(name, "push") self.assertTrue(action1 is signalAction) action2 = DummyUndoableAction() self.log.push(action2) self.assertEqual(len(self.signals), 3) name, (stack, signalAction) = self.signals[2] self.assertEqual(name, "push") self.assertTrue(action2 is signalAction) # commit self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.commit() self.assertEqual(len(self.signals), 4) name, (stack, nested) = self.signals[3] self.assertEqual(name, "commit") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) self.assertTrue(action1.done_) self.assertTrue(action2.done_) # undo what we just committed self.log.undo() self.assertEqual(len(self.signals), 5) name, stack = self.signals[4] self.assertEqual(name, "undo") self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 1) self.assertFalse(action1.done_) self.assertFalse(action2.done_) # redo self.log.redo() self.assertEqual(len(self.signals), 6) name, stack = self.signals[5] self.assertEqual(name, "redo") self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) self.assertTrue(action1.done_) self.assertTrue(action2.done_) def testOrder(self): """ Test that actions are undone and redone in the correct order. """ call_sequence = [] class Action(UndoableAction): def __init__(self, n): UndoableAction.__init__(self) self.n = n def do(self): call_sequence.append("do%s" % self.n) self._done() def undo(self): call_sequence.append("undo%s" % self.n) self._undone() action1 = Action(1) action2 = Action(2) action3 = Action(3) self.log.begin("meh") self.log.push(action1) self.log.begin("nested") self.log.push(action2) self.log.commit() self.log.push(action3) self.log.commit() self.log.undo() self.assertEqual(call_sequence, ["undo3", "undo2", "undo1"]) call_sequence[:] = [] self.log.redo() self.assertEqual(call_sequence, ["do1", "do2", "do3"]) call_sequence[:] = [] self.log.undo() self.assertEqual(call_sequence, ["undo3", "undo2", "undo1"])
class TestUndoableActionLog(TestCase): def setUp(self): self.log = UndoableActionLog() self._connectToUndoableActionLog(self.log) self.signals = [] def tearDown(self): self._disconnectFromUndoableActionLog(self.log) def _undoActionLogSignalCb(self, log, *args): args = list(args) signalName = args.pop(-1) self.signals.append((signalName, args)) def _connectToUndoableActionLog(self, log): for signalName in ("begin", "push", "rollback", "commit", "move"): log.connect(signalName, self._undoActionLogSignalCb, signalName) def _disconnectFromUndoableActionLog(self, log): self.log.disconnect_by_func(self._undoActionLogSignalCb) def testRollbackWrongState(self): self.assertRaises(UndoWrongStateError, self.log.rollback) def testCommitWrongState(self): self.assertRaises(UndoWrongStateError, self.log.commit, "") def testPushWrongState(self): # no error in this case self.log.push(None) def testUndoWrongState(self): self.assertRaises(UndoWrongStateError, self.log.undo) def testRedoWrongState(self): self.assertRaises(UndoWrongStateError, self.log.redo) def testCheckpoint(self): self.log.begin("meh") self.log.push(mock.Mock(spec=UndoableAction)) self.assertRaises(UndoWrongStateError, self.log.checkpoint) self.log.rollback() self.log.checkpoint() self.assertNotEqual(self.log._checkpoint, None) def testDirty(self): self.assertFalse(self.log.dirty()) self.log.begin("meh") self.log.push(mock.Mock(spec=UndoableAction)) self.log.commit("meh") self.assertTrue(self.log.dirty()) self.log.checkpoint() self.assertFalse(self.log.dirty()) self.log.undo() self.assertTrue(self.log.dirty()) self.log.redo() self.assertFalse(self.log.dirty()) def testCommit(self): """ Commit a stack. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(self.log.undo_stacks, []) self.log.push(mock.Mock(spec=UndoableAction)) self.log.commit("meh") self.assertEqual(len(self.signals), 3) name, (stack, action) = self.signals[1] self.assertEqual(name, "push") name, (stack,) = self.signals[2] self.assertEqual(name, "commit") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) def test_commit_proper(self): self.log.begin("meh") self.assertRaises(UndoWrongStateError, self.log.commit, "notmeh") def testNestedCommit(self): """ Do two nested commits. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("nested") self.assertEqual(len(self.signals), 2) name, (stack,) = self.signals[1] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(self.log.undo_stacks, []) self.log.push(mock.Mock(spec=UndoableAction)) self.log.commit("nested") self.assertEqual(len(self.signals), 4) name, (stack, action) = self.signals[2] self.assertEqual(name, "push") name, (stack,) = self.signals[3] self.assertEqual(name, "commit") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(self.log.undo_stacks, []) self.log.commit("meh") self.assertEqual(len(self.signals), 5) name, (stack,) = self.signals[4] self.assertEqual(name, "commit") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) def test_finalizing_action(self): action1 = mock.Mock() action2 = mock.Mock() with self.log.started("one", finalizing_action=action1): self.log.push(mock.Mock(spec=UndoableAction)) with self.log.started("two", finalizing_action=action2): 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() def testRollback(self): """ Test a rollback. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.log.rollback() self.assertEqual(len(self.signals), 2) name, (stack,) = self.signals[1] self.assertEqual(name, "rollback") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def testNestedRollback(self): """ Test two nested rollbacks. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("nested") self.assertEqual(len(self.signals), 2) name, (stack,) = self.signals[1] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.log.rollback() self.assertEqual(len(self.signals), 3) name, (stack,) = self.signals[2] self.assertEqual(name, "rollback") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.rollback() self.assertEqual(len(self.signals), 4) name, (stack,) = self.signals[3] self.assertEqual(name, "rollback") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def testUndoRedo(self): """ Try an undo() redo() sequence. """ # begin self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) # push two actions action1 = mock.Mock(spec=UndoableAction) action1.expand.return_value = False self.log.push(action1) self.assertEqual(len(self.signals), 2) name, (stack, signalAction) = self.signals[1] self.assertEqual(name, "push") self.assertTrue(action1 is signalAction) action2 = mock.Mock(spec=UndoableAction) self.log.push(action2) self.assertEqual(len(self.signals), 3) name, (stack, signalAction) = self.signals[2] self.assertEqual(name, "push") self.assertTrue(action2 is signalAction) # commit self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.commit("meh") self.assertEqual(len(self.signals), 4) name, (stack,) = self.signals[3] self.assertEqual(name, "commit") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(action1.do.call_count, 0) self.assertEqual(action1.undo.call_count, 0) self.assertEqual(action2.do.call_count, 0) self.assertEqual(action2.undo.call_count, 0) # undo what we just committed self.log.undo() self.assertEqual(len(self.signals), 5) name, stack = self.signals[4] self.assertEqual(name, "move") self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 1) self.assertEqual(action1.do.call_count, 0) self.assertEqual(action1.undo.call_count, 1) self.assertEqual(action2.do.call_count, 0) self.assertEqual(action2.undo.call_count, 1) # redo self.log.redo() self.assertEqual(len(self.signals), 6) name, stack = self.signals[5] self.assertEqual(name, "move") self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(action1.do.call_count, 1) self.assertEqual(action1.undo.call_count, 1) self.assertEqual(action2.do.call_count, 1) self.assertEqual(action2.undo.call_count, 1) def testOrder(self): """ Test that actions are undone and redone in the correct order. """ order = mock.Mock() order.action1 = mock.Mock(spec=UndoableAction) order.action1.expand.return_value = False order.action2 = mock.Mock(spec=UndoableAction) order.action2.expand.return_value = False order.action3 = mock.Mock(spec=UndoableAction) order.action3.expand.return_value = False with self.log.started("meh"): self.log.push(order.action1) with self.log.started("nested"): self.log.push(order.action2) self.log.push(order.action3) self.log.undo() order.assert_has_calls([mock.call.action3.undo(), mock.call.action2.undo(), mock.call.action1.undo()]) self.log.redo() order.assert_has_calls([mock.call.action1.do(), mock.call.action2.do(), mock.call.action3.do()]) self.log.undo() order.assert_has_calls([mock.call.action3.undo(), mock.call.action2.undo(), mock.call.action1.undo()]) def test_toplevel_operation(self): """Checks the toplevel operations nesting.""" self.log.begin("one", toplevel=False) self.log.commit("one") self.log.begin("two", toplevel=True) self.log.commit("two") self.log.begin("three") self.assertRaises(UndoWrongStateError, self.log.begin, "four", toplevel=True) self.log.begin("nested1") self.log.begin("nested2", toplevel=False)
class Pitivi(Gtk.Application, Loggable): """Hello world. Attributes: action_log (UndoableActionLog): The undo/redo log for the current project. effects (EffectsManager): The effects which can be applied to a clip. gui (MainWindow): The main window of the app. recent_manager (Gtk.RecentManager): Manages recently used projects. project_manager (ProjectManager): The holder of the current project. settings (GlobalSettings): The application-wide settings. system (pitivi.utils.system.System): The system running the app. """ __gsignals__ = { "version-info-received": (GObject.SignalFlags.RUN_LAST, None, (object, )) } def __init__(self): Gtk.Application.__init__(self, application_id="org.pitivi.Pitivi", flags=Gio.ApplicationFlags.NON_UNIQUE | Gio.ApplicationFlags.HANDLES_OPEN) Loggable.__init__(self) self.settings = None self.threads = None self.effects = None self.system = None self.project_manager = ProjectManager(self) self.action_log = None self.project_observer = None self._last_action_time = Gst.util_get_timestamp() self.gui = None self.recent_manager = Gtk.RecentManager.get_default() self.__inhibit_cookies = {} self._version_information = {} self._scenario_file = None self._first_action = True Zoomable.app = self self.shortcuts = ShortcutsManager(self) def write_action(self, action, **kwargs): if self._scenario_file is None: return if self._first_action: self._scenario_file.write( "description, seek=true, handles-states=true\n") self._first_action = False now = Gst.util_get_timestamp() if now - self._last_action_time > 0.05 * Gst.SECOND: # We need to make sure that the waiting time was more than 50 ms. st = Gst.Structure.new_empty("wait") st["duration"] = float((now - self._last_action_time) / Gst.SECOND) self._scenario_file.write(st.to_string()) self._scenario_file.write("\n") self._last_action_time = now if not isinstance(action, Gst.Structure): structure = Gst.Structure.new_empty(action) for key, value in kwargs.items(): key = key.replace("_", "-") structure[key] = value action = structure self._scenario_file.write(action.to_string()) self._scenario_file.write("\n") self._scenario_file.flush() def do_startup(self): Gtk.Application.do_startup(self) # Init logging as early as possible so we can log startup code enable_color = os.environ.get('PITIVI_DEBUG_NO_COLOR', '0') not in ('', '1') # Let's show a human-readable Pitivi debug output by default, and only # show a crazy unreadable mess when surrounded by gst debug statements. enable_crack_output = "GST_DEBUG" in os.environ loggable.init('PITIVI_DEBUG', enable_color, enable_crack_output) self.info('starting up') self._setup() self._check_version() def _setup(self): # pylint: disable=attribute-defined-outside-init self.settings = GlobalSettings() self.threads = ThreadMaster() self.effects = EffectsManager() self.proxy_manager = ProxyManager(self) self.system = get_system() self.plugin_manager = PluginManager(self) self.project_manager.connect("new-project-loaded", self._new_project_loaded_cb) self.project_manager.connect_after("project-closed", self._project_closed_cb) self.project_manager.connect("project-saved", self.__project_saved_cb) self._create_actions() self._sync_do_undo() def _create_actions(self): self.shortcuts.register_group("app", _("General"), position=10) # pylint: disable=attribute-defined-outside-init self.undo_action = Gio.SimpleAction.new("undo", None) self.undo_action.connect("activate", self._undo_cb) self.add_action(self.undo_action) self.shortcuts.add("app.undo", ["<Primary>z"], self.undo_action, _("Undo the most recent action")) self.redo_action = Gio.SimpleAction.new("redo", None) self.redo_action.connect("activate", self._redo_cb) self.add_action(self.redo_action) self.shortcuts.add("app.redo", ["<Primary><Shift>z"], self.redo_action, _("Redo the most recent action")) self.quit_action = Gio.SimpleAction.new("quit", None) self.quit_action.connect("activate", self._quit_cb) self.add_action(self.quit_action) self.shortcuts.add("app.quit", ["<Primary>q"], self.quit_action, _("Quit")) self.show_shortcuts_action = Gio.SimpleAction.new( "shortcuts_window", None) self.show_shortcuts_action.connect("activate", self._show_shortcuts_cb) self.add_action(self.show_shortcuts_action) self.shortcuts.add("app.shortcuts_window", ["<Primary>F1", "<Primary>question"], self.show_shortcuts_action, _("Show the Shortcuts Window")) def do_activate(self): if self.gui: # The app is already started and the window already created. # Present the already existing window. if self.system.has_x11(): # TODO: Use present() instead of present_with_time() when # https://bugzilla.gnome.org/show_bug.cgi?id=688830 is fixed. from gi.repository import GdkX11 x11_server_time = GdkX11.x11_get_server_time( self.gui.get_window()) self.gui.present_with_time(x11_server_time) else: # On Wayland or Quartz (Mac OS X) backend there is no GdkX11, # so just use present() directly here. self.gui.present() # No need to show the welcome wizard. return self.create_main_window() self.gui.show_perspective(self.gui.greeter) def create_main_window(self): if self.gui: return self.gui = MainWindow(self) self.gui.setup_ui() self.add_window(self.gui) def do_open(self, giofiles, unused_count, unused_hint): assert giofiles self.create_main_window() if len(giofiles) > 1: self.warning( "Opening only one project at a time. Ignoring the rest!") project_file = giofiles[0] self.project_manager.load_project(quote_uri(project_file.get_uri())) return True def shutdown(self): """Closes the app. Returns: bool: True if successful, False otherwise. """ self.debug("shutting down") # Refuse to close if we are not done with the current project. if not self.project_manager.close_running_project(): self.warning( "Not closing since running project doesn't want to close") return False if self.gui: self.gui.destroy() self.threads.wait_all_threads() self.settings.store_settings() self.quit() return True def _set_scenario_file(self, uri): if uri: project_path = path_from_uri(uri) else: # New project. project_path = None if 'PITIVI_SCENARIO_FILE' in os.environ: scenario_path = os.environ['PITIVI_SCENARIO_FILE'] else: cache_dir = xdg_cache_home("scenarios") scenario_name = str(time.strftime("%Y%m%d-%H%M%S")) if project_path: scenario_name += os.path.splitext( project_path.replace(os.sep, "_"))[0] scenario_path = os.path.join(cache_dir, scenario_name + ".scenario") scenario_path = path_from_uri(quote_uri(scenario_path)) self._scenario_file = open(scenario_path, "w") if project_path and not project_path.endswith(".scenario"): # It's an xges file probably. with open(project_path) as project: content = project.read().replace("\n", "") self.write_action("load-project", serialized_content=content) def _new_project_loaded_cb(self, unused_project_manager, project): uri = project.get_uri() if uri: # We remove the project from recent projects list # and then re-add it to this list to make sure it # gets positioned at the top of the recent projects list. try: self.recent_manager.remove_item(uri) except GLib.Error as e: if e.domain != "gtk-recent-manager-error-quark": raise e self.recent_manager.add_item(uri) self.action_log = UndoableActionLog() self.action_log.connect("pre-push", self._action_log_pre_push_cb) self.action_log.connect("commit", self._action_log_commit) self.action_log.connect("move", self._action_log_move_cb) self.project_observer = ProjectObserver(project, self.action_log) self._set_scenario_file(project.get_uri()) def __project_saved_cb(self, unused_project_manager, unused_project, uri): if uri: self.recent_manager.add_item(uri) def _project_closed_cb(self, unused_project_manager, project): if project.loaded: self.action_log = None self._sync_do_undo() if self._scenario_file: self.write_action("stop") self._scenario_file.close() self._scenario_file = None def _check_version(self): """Checks online for new versions of the app.""" self.info("Requesting version information async") giofile = Gio.File.new_for_uri(RELEASES_URL) giofile.load_contents_async(None, self._version_info_received_cb, None) def _version_info_received_cb(self, giofile, result, user_data): # pylint: disable=broad-except try: raw = giofile.load_contents_finish(result)[1] if not isinstance(raw, str): raw = raw.decode() raw = raw.split("\n") # Split line at '=' if the line is not empty or a comment line data = [ element.split("=") for element in raw if element and not element.startswith("#") ] # search newest version and status status = "UNSUPPORTED" current_version = None for version, version_status in data: if VERSION == version: status = version_status if version_status.upper() == "CURRENT": # This is the latest. current_version = version self.info("Latest software version is %s", current_version) version_split = [int(i) for i in VERSION.split(".")] current_version_split = [ int(i) for i in current_version.split(".") ] if version_split > current_version_split: status = "CURRENT" self.info( "Running version %s, which is newer than the latest known version. Considering it as the latest current version.", VERSION) elif status == "UNSUPPORTED": self.warning("Using an outdated version of Pitivi (%s)", VERSION) self._version_information["current"] = current_version self._version_information["status"] = status self.emit("version-info-received", self._version_information) except Exception as e: self.warning("Version info could not be read: %s", e) def is_latest(self): """Whether the app's version is the latest as far as we know.""" status = self._version_information.get("status") return status is None or status.upper() == "CURRENT" def get_latest(self): """Get the latest version of the app or None.""" return self._version_information.get("current") def _quit_cb(self, unused_action, unused_param): self.shutdown() def _undo_cb(self, unused_action, unused_param): self.action_log.undo() def _redo_cb(self, unused_action, unused_param): self.action_log.redo() def _show_shortcuts_cb(self, unused_action, unused_param): show_shortcuts(self) def _action_log_pre_push_cb(self, unused_action_log, action): scenario_action = action.as_scenario_action() if scenario_action: self.write_action(scenario_action) def _action_log_commit(self, action_log, unused_stack): if action_log.is_in_transaction(): return self._sync_do_undo() def _action_log_move_cb(self, action_log, unused_stack): self._sync_do_undo() def _sync_do_undo(self): can_undo = self.action_log and bool(self.action_log.undo_stacks) self.undo_action.set_enabled(bool(can_undo)) can_redo = self.action_log and bool(self.action_log.redo_stacks) self.redo_action.set_enabled(bool(can_redo)) if not self.project_manager.current_project: return dirty = self.action_log and self.action_log.dirty() self.project_manager.current_project.set_modification_state(dirty) # In the tests we do not want to create any gui if self.gui is not None: self.gui.editor.show_project_status() def simple_inhibit(self, reason, flags): """Informs the session manager about actions to be inhibited. Keeps track of the reasons received. A specific reason should always be accompanied by the same flags. Calling the method a second time with the same reason has no effect unless `simple_uninhibit` has been called in the meanwhile. Args: reason (str): The reason for which to perform the inhibition. flags (Gtk.ApplicationInhibitFlags): What should be inhibited. """ if reason in self.__inhibit_cookies: self.debug("Inhibit reason already active: %s", reason) return self.debug("Inhibiting %s for %s", flags, reason) cookie = self.inhibit(self.gui, flags, reason) self.__inhibit_cookies[reason] = cookie def simple_uninhibit(self, reason): """Informs the session manager that an inhibition is not needed anymore. Args: reason (str): The reason which is not valid anymore. """ try: cookie = self.__inhibit_cookies.pop(reason) except KeyError: self.debug("Inhibit reason not active: %s", reason) return self.debug("Uninhibiting %s", reason) self.uninhibit(cookie)
class TestUndoableActionLog(common.TestCase): """Tests for the UndoableActionLog class.""" def setUp(self): self.log = UndoableActionLog() self._connect_to_undoable_action_log(self.log) self.signals = [] def tearDown(self): self._disconnect_from_undoable_action_log() def check_signals(self, *expected_signals): signals = [item[0] for item in self.signals] self.assertListEqual(signals, list(expected_signals)) def _undo_action_log_signal_cb(self, log, *args): args = list(args) signal_name = args.pop(-1) self.signals.append((signal_name, args)) def _connect_to_undoable_action_log(self, log): for signal_name in ("begin", "push", "rollback", "commit", "move"): log.connect(signal_name, self._undo_action_log_signal_cb, signal_name) def _disconnect_from_undoable_action_log(self): self.log.disconnect_by_func(self._undo_action_log_signal_cb) def test_rollback_wrong_state(self): self.assertRaises(UndoWrongStateError, self.log.rollback) def test_commit_wrong_state(self): self.assertRaises(UndoWrongStateError, self.log.commit, "") def test_push_wrong_state(self): # no error in this case self.log.push(None) def test_undo_wrong_state(self): self.assertRaises(UndoWrongStateError, self.log.undo) def test_redo_wrong_state(self): self.assertRaises(UndoWrongStateError, self.log.redo) def test_checkpoint(self): self.log.begin("meh") self.log.push(mock.Mock(spec=UndoableAction)) self.assertRaises(UndoWrongStateError, self.log.checkpoint) self.log.rollback() self.log.checkpoint() self.assertNotEqual(self.log._checkpoint, None) def test_dirty(self): self.assertFalse(self.log.dirty()) self.log.begin("meh") self.log.push(mock.Mock(spec=UndoableAction)) self.log.commit("meh") self.assertTrue(self.log.dirty()) self.log.checkpoint() self.assertFalse(self.log.dirty()) self.log.undo() self.assertTrue(self.log.dirty()) self.log.redo() self.assertFalse(self.log.dirty()) def test_commit(self): """Checks committing a stack.""" self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (_stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(self.log.undo_stacks, []) self.log.push(mock.Mock(spec=UndoableAction)) self.log.commit("meh") self.assertEqual(len(self.signals), 3) name, (_stack, _action) = self.signals[1] self.assertEqual(name, "push") name, (_stack,) = self.signals[2] self.assertEqual(name, "commit") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) def test_commit_proper(self): self.log.begin("meh") self.assertRaises(UndoWrongStateError, self.log.commit, "notmeh") def test_nested_commit(self): """Checks two nested commits.""" self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (_stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("nested") self.assertEqual(len(self.signals), 2) name, (_stack,) = self.signals[1] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(self.log.undo_stacks, []) self.log.push(mock.Mock(spec=UndoableAction)) self.log.commit("nested") self.assertEqual(len(self.signals), 4) name, (_stack, _action) = self.signals[2] self.assertEqual(name, "push") name, (_stack,) = self.signals[3] self.assertEqual(name, "commit") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(self.log.undo_stacks, []) self.log.commit("meh") self.assertEqual(len(self.signals), 5) name, (_stack,) = self.signals[4] self.assertEqual(name, "commit") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) def test_finalizing_action(self): action1 = mock.Mock() action2 = mock.Mock() with self.log.started("one", finalizing_action=action1): self.log.push(mock.Mock(spec=UndoableAction)) with self.log.started("two", finalizing_action=action2): 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() def test_rollback(self): """Checks a rollback.""" self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.check_signals("begin") name, (_stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) action = mock.Mock(spec=UndoableAction) self.log.push(action) self.log.rollback() action.undo.assert_called_once_with() self.check_signals("begin", "push", "rollback") name, (_stack,) = self.signals[2] self.assertEqual(name, "rollback") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def test_rollback_noop(self): """Checks a rollback which does not act.""" self.log.begin("meh") action = mock.Mock(spec=UndoableAction) self.log.push(action) self.log.rollback(undo=False) action.undo.assert_not_called() def test_nested_rollback(self): """Checks two nested rollbacks.""" self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (_stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("nested") self.assertEqual(len(self.signals), 2) name, (_stack,) = self.signals[1] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.log.rollback() self.assertEqual(len(self.signals), 3) name, (_stack,) = self.signals[2] self.assertEqual(name, "rollback") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.rollback() self.assertEqual(len(self.signals), 4) name, (_stack,) = self.signals[3] self.assertEqual(name, "rollback") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def test_undo_redo(self): """Tries an undo() redo() sequence.""" # begin self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (_stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) # push two actions action1 = mock.Mock(spec=UndoableAction) action1.expand.return_value = False self.log.push(action1) self.assertEqual(len(self.signals), 2) name, (_stack, signal_action) = self.signals[1] self.assertEqual(name, "push") self.assertTrue(action1 is signal_action) action2 = mock.Mock(spec=UndoableAction) self.log.push(action2) self.assertEqual(len(self.signals), 3) name, (_stack, signal_action) = self.signals[2] self.assertEqual(name, "push") self.assertTrue(action2 is signal_action) # commit self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.commit("meh") self.assertEqual(len(self.signals), 4) name, (_stack,) = self.signals[3] self.assertEqual(name, "commit") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(action1.do.call_count, 0) self.assertEqual(action1.undo.call_count, 0) self.assertEqual(action2.do.call_count, 0) self.assertEqual(action2.undo.call_count, 0) # undo what we just committed self.log.undo() self.assertEqual(len(self.signals), 5) name, _args = self.signals[4] self.assertEqual(name, "move") self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 1) self.assertEqual(action1.do.call_count, 0) self.assertEqual(action1.undo.call_count, 1) self.assertEqual(action2.do.call_count, 0) self.assertEqual(action2.undo.call_count, 1) # redo self.log.redo() self.assertEqual(len(self.signals), 6) name, _args = self.signals[5] self.assertEqual(name, "move") self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(action1.do.call_count, 1) self.assertEqual(action1.undo.call_count, 1) self.assertEqual(action2.do.call_count, 1) self.assertEqual(action2.undo.call_count, 1) def test_order(self): """Checks actions are undone and redone in the correct order.""" order = mock.Mock() order.action1 = mock.Mock(spec=UndoableAction) order.action1.expand.return_value = False order.action2 = mock.Mock(spec=UndoableAction) order.action2.expand.return_value = False order.action3 = mock.Mock(spec=UndoableAction) order.action3.expand.return_value = False with self.log.started("meh"): self.log.push(order.action1) with self.log.started("nested"): self.log.push(order.action2) self.log.push(order.action3) self.log.undo() order.assert_has_calls([mock.call.action3.undo(), mock.call.action2.undo(), mock.call.action1.undo()]) self.log.redo() order.assert_has_calls([mock.call.action1.do(), mock.call.action2.do(), mock.call.action3.do()]) self.log.undo() order.assert_has_calls([mock.call.action3.undo(), mock.call.action2.undo(), mock.call.action1.undo()]) def test_toplevel_operation(self): """Checks the toplevel operations nesting.""" self.log.begin("one", toplevel=False) self.log.commit("one") self.log.begin("two", toplevel=True) self.log.commit("two") self.log.begin("three") self.assertRaises(UndoWrongStateError, self.log.begin, "four", toplevel=True) self.log.begin("nested1") self.log.begin("nested2", toplevel=False) def test_failing_operation_rollback(self): """Checks that failing operations are rolled back.""" action = mock.Mock(spec=UndoableAction) class WatchingError(Exception): pass with self.assertRaises(WatchingError): with self.log.started("failing_op"): self.log.push(action) raise WatchingError() # Check the rollback happened self.assertEqual(action.do.call_count, 0) self.assertEqual(action.undo.call_count, 1) # Check the undo and redo stacks are empty self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def test_merging(self): with self.log.started("one", mergeable=False): action = mock.Mock(spec=UndoableAction) action.expand.side_effect = Exception("should not have been called") self.log.push(action) self.assertEqual(len(self.log.undo_stacks), 1) with self.log.started("one", mergeable=True): action = mock.Mock(spec=UndoableAction) action.expand.return_value = False self.log.push(action) self.assertEqual(len(self.log.undo_stacks), 2) with self.log.started("one", mergeable=True): action = mock.Mock(spec=UndoableAction) action.expand.return_value = True self.log.push(action) self.assertEqual(len(self.log.undo_stacks), 3) with self.log.started("one", mergeable=True): action = mock.Mock(spec=UndoableAction) action.expand.return_value = True self.log.push(action) self.assertEqual(len(self.log.undo_stacks), 3) with self.log.started("one", mergeable=False): action = mock.Mock(spec=UndoableAction) action.expand.side_effect = Exception("should not have been called") self.log.push(action) self.assertEqual(len(self.log.undo_stacks), 4)
class TestUndoableActionLog(TestCase): def setUp(self): self.log = UndoableActionLog() self._connectToUndoableActionLog(self.log) self.signals = [] def tearDown(self): self._disconnectFromUndoableActionLog(self.log) def _undoActionLogSignalCb(self, log, *args): args = list(args) signalName = args.pop(-1) self.signals.append((signalName, args)) def _connectToUndoableActionLog(self, log): for signalName in ("begin", "push", "rollback", "commit", "move"): log.connect(signalName, self._undoActionLogSignalCb, signalName) def _disconnectFromUndoableActionLog(self, log): self.log.disconnect_by_func(self._undoActionLogSignalCb) def testRollbackWrongState(self): self.assertRaises(UndoWrongStateError, self.log.rollback) def testCommitWrongState(self): self.assertRaises(UndoWrongStateError, self.log.commit, "") def testPushWrongState(self): # no error in this case self.log.push(None) def testUndoWrongState(self): self.assertRaises(UndoWrongStateError, self.log.undo) def testRedoWrongState(self): self.assertRaises(UndoWrongStateError, self.log.redo) def testCheckpoint(self): self.log.begin("meh") self.log.push(mock.Mock(spec=UndoableAction)) self.assertRaises(UndoWrongStateError, self.log.checkpoint) self.log.rollback() self.log.checkpoint() self.assertNotEqual(self.log._checkpoint, None) def testDirty(self): self.assertFalse(self.log.dirty()) self.log.begin("meh") self.log.push(mock.Mock(spec=UndoableAction)) self.log.commit("meh") self.assertTrue(self.log.dirty()) self.log.checkpoint() self.assertFalse(self.log.dirty()) self.log.undo() self.assertTrue(self.log.dirty()) self.log.redo() self.assertFalse(self.log.dirty()) def testCommit(self): """ Commit a stack. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, ) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(self.log.undo_stacks, []) self.log.push(mock.Mock()) self.log.commit("meh") self.assertEqual(len(self.signals), 3) name, (stack, action) = self.signals[1] self.assertEqual(name, "push") name, (stack, ) = self.signals[2] self.assertEqual(name, "commit") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) def test_commit_proper(self): self.log.begin("meh") self.assertRaises(UndoWrongStateError, self.log.commit, "notmeh") def testNestedCommit(self): """ Do two nested commits. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, ) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("nested") self.assertEqual(len(self.signals), 2) name, (stack, ) = self.signals[1] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(self.log.undo_stacks, []) self.log.push(mock.Mock()) self.log.commit("nested") self.assertEqual(len(self.signals), 4) name, (stack, action) = self.signals[2] self.assertEqual(name, "push") name, (stack, ) = self.signals[3] self.assertEqual(name, "commit") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(self.log.undo_stacks, []) self.log.commit("meh") self.assertEqual(len(self.signals), 5) name, (stack, ) = self.signals[4] self.assertEqual(name, "commit") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) def test_finalizing_action(self): action1 = mock.Mock() action2 = mock.Mock() with self.log.started("one", finalizing_action=action1): self.log.push(mock.Mock()) with self.log.started("two", finalizing_action=action2): self.log.push(mock.Mock()) action1.do.assert_called_once_with() # For now, we call the finalizing action only for the top stack. action2.do.assert_not_called() def testRollback(self): """ Test a rollback. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, ) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.log.rollback() self.assertEqual(len(self.signals), 2) name, (stack, ) = self.signals[1] self.assertEqual(name, "rollback") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def testNestedRollback(self): """ Test two nested rollbacks. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, ) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("nested") self.assertEqual(len(self.signals), 2) name, (stack, ) = self.signals[1] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) self.log.rollback() self.assertEqual(len(self.signals), 3) name, (stack, ) = self.signals[2] self.assertEqual(name, "rollback") self.assertTrue(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.rollback() self.assertEqual(len(self.signals), 4) name, (stack, ) = self.signals[3] self.assertEqual(name, "rollback") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def testUndoRedo(self): """ Try an undo() redo() sequence. """ # begin self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, ) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) # push two actions action1 = mock.Mock(spec=UndoableAction) self.log.push(action1) self.assertEqual(len(self.signals), 2) name, (stack, signalAction) = self.signals[1] self.assertEqual(name, "push") self.assertTrue(action1 is signalAction) action2 = mock.Mock(spec=UndoableAction) self.log.push(action2) self.assertEqual(len(self.signals), 3) name, (stack, signalAction) = self.signals[2] self.assertEqual(name, "push") self.assertTrue(action2 is signalAction) # commit self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.commit("meh") self.assertEqual(len(self.signals), 4) name, (stack, ) = self.signals[3] self.assertEqual(name, "commit") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(action1.do.call_count, 0) self.assertEqual(action1.undo.call_count, 0) self.assertEqual(action2.do.call_count, 0) self.assertEqual(action2.undo.call_count, 0) # undo what we just committed self.log.undo() self.assertEqual(len(self.signals), 5) name, stack = self.signals[4] self.assertEqual(name, "move") self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 1) self.assertEqual(action1.do.call_count, 0) self.assertEqual(action1.undo.call_count, 1) self.assertEqual(action2.do.call_count, 0) self.assertEqual(action2.undo.call_count, 1) # redo self.log.redo() self.assertEqual(len(self.signals), 6) name, stack = self.signals[5] self.assertEqual(name, "move") self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(action1.do.call_count, 1) self.assertEqual(action1.undo.call_count, 1) self.assertEqual(action2.do.call_count, 1) self.assertEqual(action2.undo.call_count, 1) def testOrder(self): """ Test that actions are undone and redone in the correct order. """ order = mock.Mock() order.action1 = mock.Mock(spec=UndoableAction) order.action2 = mock.Mock(spec=UndoableAction) order.action3 = mock.Mock(spec=UndoableAction) with self.log.started("meh"): self.log.push(order.action1) with self.log.started("nested"): self.log.push(order.action2) self.log.push(order.action3) self.log.undo() order.assert_has_calls([ mock.call.action3.undo(), mock.call.action2.undo(), mock.call.action1.undo() ]) self.log.redo() order.assert_has_calls([ mock.call.action1.do(), mock.call.action2.do(), mock.call.action3.do() ]) self.log.undo() order.assert_has_calls([ mock.call.action3.undo(), mock.call.action2.undo(), mock.call.action1.undo() ])
class TestTimelineUndo(TestCase): def setUp(self): app = Pitivi() app._startupCb(app) app.project_manager.newBlankProject() self.timeline = app.project_manager.current_project.timeline self.layer = GES.Layer() self.timeline.add_layer(self.layer) self.action_log = UndoableActionLog() self.observer = TimelineLogObserverSpy(self.action_log) self.observer.startObserving(self.timeline) def getTimelineClips(self): for layer in self.timeline.layers: for clip in layer.get_clips(): yield clip @staticmethod def commitCb(action_log, stack, nested, stacks): stacks.append(stack) def testAddClip(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.action_log.begin("add clip") self.layer.add_clip(clip1) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions)) action = stack.done_actions[0] self.assertTrue(isinstance(action, ClipAdded)) self.assertTrue(clip1 in self.getTimelineClips()) self.action_log.undo() self.assertFalse(clip1 in self.getTimelineClips()) self.action_log.redo() self.assertTrue(clip1 in self.getTimelineClips()) def testRemoveClip(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.layer.add_clip(clip1) self.action_log.begin("remove clip") self.layer.remove_clip(clip1) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions)) action = stack.done_actions[0] self.assertTrue(isinstance(action, ClipRemoved)) self.assertFalse(clip1 in self.getTimelineClips()) self.action_log.undo() self.assertTrue(clip1 in self.getTimelineClips()) self.action_log.redo() self.assertFalse(clip1 in self.getTimelineClips()) def testAddEffectToClip(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.layer.add_clip(clip1) effect1 = GES.Effect.new("agingtv") self.action_log.begin("add effect") clip1.add(effect1) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions), stack.done_actions) action = stack.done_actions[0] self.assertTrue(isinstance(action, TrackElementAdded)) self.assertTrue(effect1 in clip1.get_children(True)) self.assertEqual(1, len([effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect)])) self.action_log.undo() self.assertFalse(effect1 in clip1.get_children(True)) self.action_log.redo() self.assertEqual(1, len([effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect)])) def testRemoveEffectFromClip(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.layer.add_clip(clip1) effect1 = GES.Effect.new("agingtv") self.action_log.begin("add effect") clip1.add(effect1) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions), stack.done_actions) action = stack.done_actions[0] self.assertTrue(isinstance(action, TrackElementAdded)) self.assertTrue(effect1 in clip1.get_children(True)) self.assertEqual(1, len([effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect)])) self.action_log.begin("remove effect") clip1.remove(effect1) self.action_log.commit() self.assertEqual(0, len([effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect)])) self.action_log.undo() self.assertEqual(1, len([effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect)])) self.action_log.redo() self.assertEqual(0, len([effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect)])) def testChangeEffectProperty(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.layer.add_clip(clip1) effect1 = GES.Effect.new("agingtv") self.action_log.begin("add effect") clip1.add(effect1) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions), stack.done_actions) action = stack.done_actions[0] self.assertTrue(isinstance(action, TrackElementAdded)) self.assertTrue(effect1 in clip1.get_children(True)) self.assertEqual(1, len([effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect)])) self.action_log.begin("change child property") effect1.set_child_property("scratch-lines", 0) self.action_log.commit() self.assertEqual(effect1.get_child_property("scratch-lines")[1], 0) self.action_log.undo() self.assertEqual(effect1.get_child_property("scratch-lines")[1], 7) self.action_log.redo() self.assertEqual(effect1.get_child_property("scratch-lines")[1], 0) self.action_log.undo() self.assertTrue(effect1 in clip1.get_children(True)) self.action_log.undo() self.assertFalse(effect1 in clip1.get_children(True)) def testClipPropertyChange(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.layer.add_clip(clip1) clip1.set_start(5 * Gst.SECOND) clip1.set_duration(20 * Gst.SECOND) self.layer.add_clip(clip1) self.action_log.begin("modify clip") clip1.set_start(10 * Gst.SECOND) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions)) action = stack.done_actions[0] self.assertTrue(isinstance(action, ClipPropertyChanged)) self.assertEqual(10 * Gst.SECOND, clip1.get_start()) self.action_log.undo() self.assertEqual(5 * Gst.SECOND, clip1.get_start()) self.action_log.redo() self.assertEqual(10 * Gst.SECOND, clip1.get_start()) clip1.set_priority(10) self.action_log.begin("priority change") clip1.set_priority(20) self.action_log.commit() self.assertEqual(20, clip1.get_priority()) self.action_log.undo() self.assertEqual(10, clip1.get_priority()) self.action_log.redo() self.assertEqual(20, clip1.get_priority()) def testUngroup(self): uri = common.getSampleUri("tears_of_steel.webm") asset = GES.UriClipAsset.request_sync(uri) clip1 = asset.extract() self.layer.add_clip(clip1) clip1.set_start(5 * Gst.SECOND) clip1.set_duration(0.5 * Gst.SECOND) timeline_clips = list(self.getTimelineClips()) self.assertEqual(1, len(timeline_clips), timeline_clips) self.assertEqual(5 * Gst.SECOND, timeline_clips[0].get_start()) self.assertEqual(0.5 * Gst.SECOND, timeline_clips[0].get_duration()) self.action_log.begin("ungroup") ungrouped = GES.Container.ungroup(clip1, False) self.assertEqual(2, len(ungrouped), ungrouped) self.action_log.commit() timeline_clips = list(self.getTimelineClips()) self.assertEqual(2, len(timeline_clips), timeline_clips) self.assertEqual(5 * Gst.SECOND, timeline_clips[0].get_start()) self.assertEqual(0.5 * Gst.SECOND, timeline_clips[0].get_duration()) self.assertEqual(5 * Gst.SECOND, timeline_clips[1].get_start()) self.assertEqual(0.5 * Gst.SECOND, timeline_clips[1].get_duration()) self.action_log.undo() timeline_clips = list(self.getTimelineClips()) self.assertEqual(1, len(timeline_clips)) self.assertEqual(5 * Gst.SECOND, timeline_clips[0].get_start()) self.assertEqual(0.5 * Gst.SECOND, timeline_clips[0].get_duration()) def testSplitClip(self): clip = GES.TitleClip() clip.set_start(0 * Gst.SECOND) clip.set_duration(20 * Gst.SECOND) self.layer.add_clip(clip) self.action_log.begin("split clip") clip1 = clip.split(10 * Gst.SECOND) self.assertEqual(2, len(self.layer.get_clips())) self.action_log.commit() self.action_log.begin("split clip") clip2 = clip1.split(15 * Gst.SECOND) self.assertEqual(3, len(self.layer.get_clips())) self.action_log.commit() self.action_log.undo() self.assertEqual(2, len(self.layer.get_clips())) self.action_log.undo() self.assertEqual(1, len(self.layer.get_clips())) self.action_log.redo() self.assertEqual(2, len(self.layer.get_clips())) self.action_log.redo() self.assertEqual(3, len(self.layer.get_clips()))
class Pitivi(Gtk.Application, Loggable): """Hello world. Attributes: action_log (UndoableActionLog): The undo/redo log for the current project. effects (EffectsManager): The effects which can be applied to a clip. gui (MainWindow): The main window of the app. project_manager (ProjectManager): The holder of the current project. settings (GlobalSettings): The application-wide settings. system (pitivi.utils.system.System): The system running the app. """ __gsignals__ = { "version-info-received": (GObject.SIGNAL_RUN_LAST, None, (object,)) } def __init__(self): Gtk.Application.__init__(self, application_id="org.pitivi", flags=Gio.ApplicationFlags.HANDLES_OPEN) Loggable.__init__(self) self.settings = None self.threads = None self.effects = None self.system = None self.project_manager = ProjectManager(self) self.action_log = None self.project_observer = None self._last_action_time = Gst.util_get_timestamp() self.gui = None self.__welcome_wizard = None self._version_information = {} self._scenario_file = None self._first_action = True Zoomable.app = self self.shortcuts = ShortcutsManager(self) def write_action(self, action, **kwargs): if self._scenario_file is None: return if self._first_action: self._scenario_file.write( "description, seek=true, handles-states=true\n") self._first_action = False now = Gst.util_get_timestamp() if now - self._last_action_time > 0.05 * Gst.SECOND: # We need to make sure that the waiting time was more than 50 ms. st = Gst.Structure.new_empty("wait") st["duration"] = float((now - self._last_action_time) / Gst.SECOND) self._scenario_file.write(st.to_string() + "\n") self._last_action_time = now if not isinstance(action, Gst.Structure): structure = Gst.Structure.new_empty(action) for key, value in kwargs.items(): key = key.replace("_", "-") structure[key] = value action = structure self._scenario_file.write(action.to_string() + "\n") self._scenario_file.flush() def do_startup(self): Gtk.Application.do_startup(self) # Init logging as early as possible so we can log startup code enable_color = not os.environ.get( 'PITIVI_DEBUG_NO_COLOR', '0') in ('', '1') # Let's show a human-readable Pitivi debug output by default, and only # show a crazy unreadable mess when surrounded by gst debug statements. enable_crack_output = "GST_DEBUG" in os.environ loggable.init('PITIVI_DEBUG', enable_color, enable_crack_output) self.info('starting up') self._setup() self._checkVersion() def _setup(self): self.settings = GlobalSettings() self.threads = ThreadMaster() self.effects = EffectsManager() self.proxy_manager = ProxyManager(self) self.system = get_system() self.project_manager.connect( "new-project-loading", self._newProjectLoadingCb) self.project_manager.connect( "new-project-loaded", self._newProjectLoaded) self.project_manager.connect("project-closed", self._projectClosed) self._createActions() self._syncDoUndo() def _createActions(self): self.shortcuts.register_group("app", _("General"), position=10) self.undo_action = Gio.SimpleAction.new("undo", None) self.undo_action.connect("activate", self._undoCb) self.add_action(self.undo_action) self.shortcuts.add("app.undo", ["<Primary>z"], _("Undo the most recent action")) self.redo_action = Gio.SimpleAction.new("redo", None) self.redo_action.connect("activate", self._redoCb) self.add_action(self.redo_action) self.shortcuts.add("app.redo", ["<Primary><Shift>z"], _("Redo the most recent action")) self.quit_action = Gio.SimpleAction.new("quit", None) self.quit_action.connect("activate", self._quitCb) self.add_action(self.quit_action) self.shortcuts.add("app.quit", ["<Primary>q"], _("Quit")) self.show_shortcuts_action = Gio.SimpleAction.new("shortcuts_window", None) self.show_shortcuts_action.connect("activate", self._show_shortcuts_cb) self.add_action(self.show_shortcuts_action) self.shortcuts.add("app.shortcuts_window", ["<Primary>F1", "<Primary>question"], _("Show the Shortcuts Window")) def do_activate(self): if self.gui: # The app is already started and the window already created. # Present the already existing window. if self.system.has_x11(): # TODO: Use present() instead of present_with_time() when # https://bugzilla.gnome.org/show_bug.cgi?id=688830 is fixed. from gi.repository import GdkX11 x11_server_time = GdkX11.x11_get_server_time(self.gui.get_window()) self.gui.present_with_time(x11_server_time) else: # On Wayland or Quartz (Mac OS X) backend there is no GdkX11, # so just use present() directly here. self.gui.present() # No need to show the welcome wizard. return self.createMainWindow() self.welcome_wizard.show() @property def welcome_wizard(self): if not self.__welcome_wizard: self.__welcome_wizard = StartUpWizard(self) return self.__welcome_wizard def createMainWindow(self): if self.gui: return self.gui = MainWindow(self) self.add_window(self.gui) self.gui.checkScreenConstraints() # We might as well show it. self.gui.show() def do_open(self, giofiles, unused_count, unused_hint): assert giofiles self.createMainWindow() if len(giofiles) > 1: self.warning( "Can open only one project file at a time. Ignoring the rest!") project_file = giofiles[0] self.project_manager.loadProject(quote_uri(project_file.get_uri())) return True def shutdown(self): """Closes the app. Returns: bool: True if successful, False otherwise. """ self.debug("shutting down") # Refuse to close if we are not done with the current project. if not self.project_manager.closeRunningProject(): self.warning( "Not closing since running project doesn't want to close") return False if self.welcome_wizard: self.welcome_wizard.hide() if self.gui: self.gui.destroy() self.threads.stopAllThreads() self.settings.storeSettings() self.quit() return True def _setScenarioFile(self, uri): if uri: project_path = path_from_uri(uri) else: # New project. project_path = None if 'PITIVI_SCENARIO_FILE' in os.environ: scenario_path = os.environ['PITIVI_SCENARIO_FILE'] else: cache_dir = get_dir(os.path.join(xdg_cache_home(), "scenarios")) scenario_name = str(time.strftime("%Y%m%d-%H%M%S")) if project_path: scenario_name += os.path.splitext(project_path.replace(os.sep, "_"))[0] scenario_path = os.path.join(cache_dir, scenario_name + ".scenario") scenario_path = path_from_uri(quote_uri(scenario_path)) self._scenario_file = open(scenario_path, "w") if project_path and not project_path.endswith(".scenario"): # It's an xges file probably. with open(project_path) as project: content = project.read().replace("\n", "") self.write_action("load-project", serialized_content=content) def _newProjectLoadingCb(self, unused_project_manager, project): self._setScenarioFile(project.get_uri()) def _newProjectLoaded(self, unused_project_manager, project): self.action_log = UndoableActionLog() self.action_log.connect("pre-push", self._action_log_pre_push_cb) self.action_log.connect("commit", self._actionLogCommit) self.action_log.connect("move", self._action_log_move_cb) self.project_observer = ProjectObserver(project, self.action_log) def _projectClosed(self, unused_project_manager, project): if project.loaded: self.action_log = None self._syncDoUndo() if self._scenario_file: self.write_action("stop") self._scenario_file.close() self._scenario_file = None def _checkVersion(self): """Checks online for new versions of the app.""" self.info("Requesting version information async") giofile = Gio.File.new_for_uri(RELEASES_URL) giofile.load_contents_async(None, self._versionInfoReceivedCb, None) def _versionInfoReceivedCb(self, giofile, result, user_data): try: raw = giofile.load_contents_finish(result)[1] if not isinstance(raw, str): raw = raw.decode() raw = raw.split("\n") # Split line at '=' if the line is not empty or a comment line data = [element.split("=") for element in raw if element and not element.startswith("#")] # search newest version and status status = "UNSUPPORTED" current_version = None for version, version_status in data: if VERSION == version: status = version_status if version_status.upper() == "CURRENT": # This is the latest. current_version = version self.info("Latest software version is %s", current_version) VERSION_split = [int(i) for i in VERSION.split(".")] current_version_split = [int(i) for i in current_version.split(".")] if VERSION_split > current_version_split: status = "CURRENT" self.info( "Running version %s, which is newer than the latest known version. Considering it as the latest current version.", VERSION) elif status is "UNSUPPORTED": self.warning( "Using an outdated version of Pitivi (%s)", VERSION) self._version_information["current"] = current_version self._version_information["status"] = status self.emit("version-info-received", self._version_information) except Exception as e: self.warning("Version info could not be read: %s", e) def isLatest(self): """Whether the app's version is the latest as far as we know.""" status = self._version_information.get("status") return status is None or status.upper() == "CURRENT" def getLatest(self): """Get the latest version of the app or None.""" return self._version_information.get("current") def _quitCb(self, unused_action, unused_param): self.shutdown() def _undoCb(self, unused_action, unused_param): self.action_log.undo() def _redoCb(self, unused_action, unused_param): self.action_log.redo() def _show_shortcuts_cb(self, unused_action, unused_param): show_shortcuts(self) def _action_log_pre_push_cb(self, unused_action_log, action): try: st = action.asScenarioAction() except NotImplementedError: self.warning("No serialization method for action %s", action) return if st: self.write_action(st) def _actionLogCommit(self, action_log, unused_stack): if action_log.is_in_transaction(): return self._syncDoUndo() def _action_log_move_cb(self, action_log, unused_stack): self._syncDoUndo() def _syncDoUndo(self): can_undo = self.action_log and bool(self.action_log.undo_stacks) self.undo_action.set_enabled(bool(can_undo)) can_redo = self.action_log and bool(self.action_log.redo_stacks) self.redo_action.set_enabled(bool(can_redo)) if not self.project_manager.current_project: return dirty = self.action_log and self.action_log.dirty() self.project_manager.current_project.setModificationState(dirty) # In the tests we do not want to create any gui if self.gui is not None: self.gui.showProjectStatus()
class TestUndoableActionLog(TestCase): def setUp(self): self.log = UndoableActionLog() self._connectToUndoableActionLog(self.log) self.signals = [] def tearDown(self): self._disconnectFromUndoableActionLog(self.log) def _undoActionLogSignalCb(self, log, *args): args = list(args) signalName = args.pop(-1) self.signals.append((signalName, args)) def _connectToUndoableActionLog(self, log): for signalName in ("begin", "push", "rollback", "commit", "undo", "redo"): log.connect(signalName, self._undoActionLogSignalCb, signalName) def _disconnectFromUndoableActionLog(self, log): self.log.disconnect_by_func(self._undoActionLogSignalCb) def testRollbackWrongState(self): self.assertRaises(UndoWrongStateError, self.log.rollback) def testCommitWrongState(self): self.assertRaises(UndoWrongStateError, self.log.commit) def testPushWrongState(self): # no error in this case self.log.push(None) def testUndoWrongState(self): self.assertRaises(UndoWrongStateError, self.log.undo) def testRedoWrongState(self): self.assertRaises(UndoWrongStateError, self.log.redo) def testCheckpoint(self): self.log.begin("meh") self.log.push(DummyUndoableAction()) self.assertRaises(UndoWrongStateError, self.log.checkpoint) self.log.rollback() self.log.checkpoint() self.assertNotEqual(self.log._checkpoint, None) def testDirty(self): self.assertFalse(self.log.dirty()) self.log.begin("meh") self.log.push(DummyUndoableAction()) self.log.commit() self.assertTrue(self.log.dirty()) self.log.checkpoint() self.assertFalse(self.log.dirty()) self.log.undo() self.assertTrue(self.log.dirty()) self.log.redo() self.assertFalse(self.log.dirty()) def testCommit(self): """ Commit a stack. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, nested) = self.signals[0] self.assertEqual(name, "begin") self.assertFalse(nested) self.assertEqual(self.log.undo_stacks, []) self.log.commit() self.assertEqual(len(self.signals), 2) name, (stack, nested) = self.signals[1] self.assertEqual(name, "commit") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) def testNestedCommit(self): """ Do two nested commits. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, nested) = self.signals[0] self.assertEqual(name, "begin") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("nested") self.assertEqual(len(self.signals), 2) name, (stack, nested) = self.signals[1] self.assertEqual(name, "begin") self.assertTrue(nested) self.assertEqual(self.log.undo_stacks, []) self.log.commit() self.assertEqual(len(self.signals), 3) name, (stack, nested) = self.signals[2] self.assertEqual(name, "commit") self.assertTrue(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.assertEqual(self.log.undo_stacks, []) self.log.commit() self.assertEqual(len(self.signals), 4) name, (stack, nested) = self.signals[3] self.assertEqual(name, "commit") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) def testRollback(self): """ Test a rollback. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, nested) = self.signals[0] self.assertEqual(name, "begin") self.assertFalse(nested) self.log.rollback() self.assertEqual(len(self.signals), 2) name, (stack, nested) = self.signals[1] self.assertEqual(name, "rollback") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def testNestedRollback(self): """ Test two nested rollbacks. """ self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, nested) = self.signals[0] self.assertEqual(name, "begin") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("nested") self.assertEqual(len(self.signals), 2) name, (stack, nested) = self.signals[1] self.assertEqual(name, "begin") self.assertTrue(nested) self.log.rollback() self.assertEqual(len(self.signals), 3) name, (stack, nested) = self.signals[2] self.assertEqual(name, "rollback") self.assertTrue(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.rollback() self.assertEqual(len(self.signals), 4) name, (stack, nested) = self.signals[3] self.assertEqual(name, "rollback") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) def testUndoRedo(self): """ Try an undo() redo() sequence. """ # begin self.log.begin("meh") self.assertEqual(len(self.signals), 1) name, (stack, nested) = self.signals[0] self.assertEqual(name, "begin") self.assertFalse(nested) # push two actions action1 = DummyUndoableAction() self.log.push(action1) self.assertEqual(len(self.signals), 2) name, (stack, signalAction) = self.signals[1] self.assertEqual(name, "push") self.assertTrue(action1 is signalAction) action2 = DummyUndoableAction() self.log.push(action2) self.assertEqual(len(self.signals), 3) name, (stack, signalAction) = self.signals[2] self.assertEqual(name, "push") self.assertTrue(action2 is signalAction) # commit self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.commit() self.assertEqual(len(self.signals), 4) name, (stack, nested) = self.signals[3] self.assertEqual(name, "commit") self.assertFalse(nested) self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) self.assertTrue(action1.done_) self.assertTrue(action2.done_) # undo what we just committed self.log.undo() self.assertEqual(len(self.signals), 5) name, stack = self.signals[4] self.assertEqual(name, "undo") self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 1) self.assertFalse(action1.done_) self.assertFalse(action2.done_) # redo self.log.redo() self.assertEqual(len(self.signals), 6) name, stack = self.signals[5] self.assertEqual(name, "redo") self.assertEqual(len(self.log.undo_stacks), 1) self.assertEqual(len(self.log.redo_stacks), 0) self.assertTrue(action1.done_) self.assertTrue(action2.done_) def testOrder(self): """ Test that actions are undone and redone in the correct order. """ call_sequence = [] class Action(UndoableAction): def __init__(self, n): self.n = n def do(self): call_sequence.append("do%s" % self.n) self._done() def undo(self): call_sequence.append("undo%s" % self.n) self._undone() action1 = Action(1) action2 = Action(2) action3 = Action(3) self.log.begin("meh") self.log.push(action1) self.log.begin("nested") self.log.push(action2) self.log.commit() self.log.push(action3) self.log.commit() self.log.undo() self.assertEqual(call_sequence, ["undo3", "undo2", "undo1"]) call_sequence[:] = [] self.log.redo() self.assertEqual(call_sequence, ["do1", "do2", "do3"]) call_sequence[:] = [] self.log.undo() self.assertEqual(call_sequence, ["undo3", "undo2", "undo1"])
class Pitivi(Gtk.Application, Loggable): """ Pitivi's application. @type effects: L{EffectsManager} @ivar gui: The main window of the app. @type gui: L{PitiviMainWindow} @ivar project_manager: The project manager object used in the application @type project_manager: L{ProjectManager} @ivar settings: Application-wide settings. @type settings: L{GlobalSettings}. """ __gsignals__ = { "version-info-received": (GObject.SIGNAL_RUN_LAST, None, (object, )) } def __init__(self): Gtk.Application.__init__(self, application_id="org.pitivi", flags=Gio.ApplicationFlags.HANDLES_OPEN) Loggable.__init__(self) self.settings = None self.threads = None self.effects = None self.system = None self.project_manager = ProjectManager(self) self.action_log = UndoableActionLog() self.timeline_log_observer = None self.project_log_observer = None self.gui = None self.welcome_wizard = None self._version_information = {} self.connect("startup", self._startupCb) self.connect("activate", self._activateCb) self.connect("open", self.openCb) def _startupCb(self, unused_app): # Init logging as early as possible so we can log startup code enable_color = not os.environ.get('PITIVI_DEBUG_NO_COLOR', '0') in ('', '1') # Let's show a human-readable Pitivi debug output by default, and only # show a crazy unreadable mess when surrounded by gst debug statements. enable_crack_output = "GST_DEBUG" in os.environ log.init('PITIVI_DEBUG', enable_color, enable_crack_output) self.info('starting up') self.settings = GlobalSettings() self.threads = ThreadMaster() self.effects = EffectsManager() self.system = getSystem() self.action_log.connect("commit", self._actionLogCommit) self.action_log.connect("undo", self._actionLogUndo) self.action_log.connect("redo", self._actionLogRedo) self.action_log.connect("cleaned", self._actionLogCleaned) self.timeline_log_observer = TimelineLogObserver(self.action_log) self.project_log_observer = ProjectLogObserver(self.action_log) self.project_manager.connect("new-project-loaded", self._newProjectLoaded) self.project_manager.connect("project-closed", self._projectClosed) self._createActions() self._checkVersion() def _createActions(self): self.undo_action = Gio.SimpleAction.new("undo", None) self.undo_action.connect("activate", self._undoCb) self.add_action(self.undo_action) self.add_accelerator("<Control>z", "app.undo", None) self.redo_action = Gio.SimpleAction.new("redo", None) self.redo_action.connect("activate", self._redoCb) self.add_action(self.redo_action) self.add_accelerator("<Control><Shift>z", "app.redo", None) self.quit_action = Gio.SimpleAction.new("quit", None) self.quit_action.connect("activate", self._quitCb) self.add_action(self.quit_action) self.add_accelerator("<Control>q", "app.quit", None) def _activateCb(self, unused_app): if self.gui: # The app is already started and the window already created. # Present the already existing window. # TODO: Use present() instead of present_with_time() when # https://bugzilla.gnome.org/show_bug.cgi?id=688830 is fixed. x11_server_time = GdkX11.x11_get_server_time(self.gui.get_window()) self.gui.present_with_time(x11_server_time) # No need to show the welcome wizard. return self.createMainWindow() self.welcome_wizard = StartUpWizard(self) self.welcome_wizard.show() def createMainWindow(self): if self.gui: return self.gui = PitiviMainWindow(self) self.add_window(self.gui) # We might as well show it. self.gui.show() def openCb(self, unused_app, giofiles, unused_count, unused_hint): assert giofiles self.createMainWindow() if len(giofiles) > 1: self.warning( "Can open only one project file at a time. Ignoring the rest!") project_file = giofiles[0] self.project_manager.loadProject(quote_uri(project_file.get_uri())) return True def shutdown(self): """ Close Pitivi. @return: C{True} if Pitivi was successfully closed, else C{False}. @rtype: C{bool} """ self.debug("shutting down") # Refuse to close if we are not done with the current project. if not self.project_manager.closeRunningProject(): self.warning( "Not closing since running project doesn't want to close") return False if self.welcome_wizard: self.welcome_wizard.hide() if self.gui: self.gui.destroy() self.threads.stopAllThreads() self.settings.storeSettings() self.quit() return True def _newProjectLoaded(self, unused_project_manager, project, unused_fully_loaded): self.action_log.clean() self.timeline_log_observer.startObserving(project.timeline) self.project_log_observer.startObserving(project) def _projectClosed(self, unused_project_manager, project): self.project_log_observer.stopObserving(project) self.timeline_log_observer.stopObserving(project.timeline) def _checkVersion(self): """ Check online for release versions information. """ self.info("Requesting version information async") giofile = Gio.File.new_for_uri(RELEASES_URL) giofile.load_contents_async(None, self._versionInfoReceivedCb, None) def _versionInfoReceivedCb(self, giofile, result, user_data): try: raw = giofile.load_contents_finish(result)[1] if not isinstance(raw, str): raw = raw.decode() raw = raw.split("\n") # Split line at '=' if the line is not empty or a comment line data = [ element.split("=") for element in raw if element and not element.startswith("#") ] # search newest version and status status = "UNSUPPORTED" current_version = None for version, version_status in data: if VERSION == version: status = version_status if version_status.upper() == "CURRENT": # This is the latest. current_version = version self.info("Latest software version is %s", current_version) VERSION_split = [int(i) for i in VERSION.split(".")] current_version_split = [ int(i) for i in current_version.split(".") ] if VERSION_split > current_version_split: status = "CURRENT" self.info( "Running version %s, which is newer than the latest known version. Considering it as the latest current version.", VERSION) elif status is "UNSUPPORTED": self.warning("Using an outdated version of Pitivi (%s)", VERSION) self._version_information["current"] = current_version self._version_information["status"] = status self.emit("version-info-received", self._version_information) except Exception as e: self.warning("Version info could not be read: %s", e) def isLatest(self): """ Whether the app's version is the latest as far as we know. """ status = self._version_information.get("status") return status is None or status.upper() == "CURRENT" def getLatest(self): """ Get the latest version of the app or None. """ return self._version_information.get("current") def _quitCb(self, unused_action, unused_param): self.shutdown() def _undoCb(self, unused_action, unused_param): self.action_log.undo() def _redoCb(self, unused_action, unused_param): self.action_log.redo() def _actionLogCommit(self, action_log, unused_stack, nested): if nested: return self._syncDoUndo(action_log) def _actionLogUndo(self, action_log, unused_stack): self._syncDoUndo(action_log) def _actionLogRedo(self, action_log, unused_stack): self._syncDoUndo(action_log) def _actionLogCleaned(self, action_log): self._syncDoUndo(action_log) def _syncDoUndo(self, action_log): can_undo = bool(action_log.undo_stacks) self.undo_action.set_enabled(can_undo) can_redo = bool(action_log.redo_stacks) self.redo_action.set_enabled(can_redo) dirty = action_log.dirty() self.project_manager.current_project.setModificationState(dirty) # In the tests we do not want to create any gui if self.gui is not None: self.gui.showProjectStatus()
class Pitivi(Gtk.Application, Loggable): """ Pitivi's application. @type effects: L{EffectsManager} @ivar gui: The main window of the app. @type gui: L{PitiviMainWindow} @ivar project_manager: The project manager object used in the application @type project_manager: L{ProjectManager} @ivar settings: Application-wide settings. @type settings: L{GlobalSettings}. """ __gsignals__ = { "version-info-received": (GObject.SIGNAL_RUN_LAST, None, (object, )) } def __init__(self): Gtk.Application.__init__(self, application_id="org.pitivi", flags=Gio.ApplicationFlags.HANDLES_OPEN) Loggable.__init__(self) self.settings = None self.threads = None self.effects = None self.system = None self.project_manager = ProjectManager(self) self.action_log = UndoableActionLog(self) self.timeline_log_observer = None self.project_log_observer = None self._last_action_time = Gst.util_get_timestamp() self.gui = None self.welcome_wizard = None self._version_information = {} self._scenario_file = None self._first_action = True self.connect("startup", self._startupCb) self.connect("activate", self._activateCb) self.connect("open", self.openCb) def write_action(self, action, properties={}): if self._first_action: self._scenario_file.write( "description, seek=true, handles-states=true\n") self._first_action = False now = Gst.util_get_timestamp() if now - self._last_action_time > 0.05 * Gst.SECOND: # We need to make sure that the waiting time was more than 50 ms. st = Gst.Structure.new_empty("wait") st["duration"] = float((now - self._last_action_time) / Gst.SECOND) self._scenario_file.write(st.to_string() + "\n") self._last_action_time = now if not isinstance(action, Gst.Structure): structure = Gst.Structure.new_empty(action) for key, value in properties.items(): structure[key] = value action = structure self._scenario_file.write(action.to_string() + "\n") self._scenario_file.flush() def _startupCb(self, unused_app): # Init logging as early as possible so we can log startup code enable_color = not os.environ.get('PITIVI_DEBUG_NO_COLOR', '0') in ('', '1') # Let's show a human-readable Pitivi debug output by default, and only # show a crazy unreadable mess when surrounded by gst debug statements. enable_crack_output = "GST_DEBUG" in os.environ log.init('PITIVI_DEBUG', enable_color, enable_crack_output) self.info('starting up') self.settings = GlobalSettings() self.threads = ThreadMaster() self.effects = EffectsManager() self.system = getSystem() self.action_log.connect("commit", self._actionLogCommit) self.action_log.connect("undo", self._actionLogUndo) self.action_log.connect("redo", self._actionLogRedo) self.action_log.connect("cleaned", self._actionLogCleaned) self.timeline_log_observer = TimelineLogObserver(self.action_log) self.project_log_observer = ProjectLogObserver(self.action_log) self.project_manager.connect("new-project-loading", self._newProjectLoadingCb) self.project_manager.connect("new-project-loaded", self._newProjectLoaded) self.project_manager.connect("project-closed", self._projectClosed) self._createActions() self._checkVersion() def _createActions(self): self.undo_action = Gio.SimpleAction.new("undo", None) self.undo_action.connect("activate", self._undoCb) self.add_action(self.undo_action) self.add_accelerator("<Control>z", "app.undo", None) self.redo_action = Gio.SimpleAction.new("redo", None) self.redo_action.connect("activate", self._redoCb) self.add_action(self.redo_action) self.add_accelerator("<Control><Shift>z", "app.redo", None) self.quit_action = Gio.SimpleAction.new("quit", None) self.quit_action.connect("activate", self._quitCb) self.add_action(self.quit_action) self.add_accelerator("<Control>q", "app.quit", None) def _activateCb(self, unused_app): if self.gui: # The app is already started and the window already created. # Present the already existing window. try: # TODO: Use present() instead of present_with_time() when # https://bugzilla.gnome.org/show_bug.cgi?id=688830 is fixed. from gi.repository import GdkX11 x11_server_time = GdkX11.x11_get_server_time( self.gui.get_window()) self.gui.present_with_time(x11_server_time) except ImportError: # On Wayland or Quartz (Mac OS X) backend there is no GdkX11, # so just use present() directly here. self.gui.present() # No need to show the welcome wizard. return self.createMainWindow() self.welcome_wizard = StartUpWizard(self) self.welcome_wizard.show() def createMainWindow(self): if self.gui: return self.gui = PitiviMainWindow(self) self.add_window(self.gui) # We might as well show it. self.gui.show() def openCb(self, unused_app, giofiles, unused_count, unused_hint): assert giofiles self.createMainWindow() if len(giofiles) > 1: self.warning( "Can open only one project file at a time. Ignoring the rest!") project_file = giofiles[0] self.project_manager.loadProject(quote_uri(project_file.get_uri())) return True def shutdown(self): """ Close Pitivi. @return: C{True} if Pitivi was successfully closed, else C{False}. @rtype: C{bool} """ self.debug("shutting down") # Refuse to close if we are not done with the current project. if not self.project_manager.closeRunningProject(): self.warning( "Not closing since running project doesn't want to close") return False if self.welcome_wizard: self.welcome_wizard.hide() if self.gui: self.gui.destroy() self.threads.stopAllThreads() self.settings.storeSettings() self.quit() return True self._first_action = True def _setScenarioFile(self, uri): if 'PITIVI_SCENARIO_FILE' in os.environ: uri = quote_uri(os.environ['PITIVI_SCENARIO_FILE']) else: cache_dir = get_dir(os.path.join(xdg_cache_home(), "scenarios")) scenario_name = str(time.strftime("%Y%m%d-%H%M%S")) project_path = None if uri: project_path = path_from_uri(uri) scenario_name += os.path.splitext( project_path.replace(os.sep, "_"))[0] uri = os.path.join(cache_dir, scenario_name + ".scenario") uri = quote_uri(uri) self._scenario_file = open(path_from_uri(uri), "w") if project_path: f = open(project_path) content = f.read() if not uri.endswith(".scenario"): self.write_action( "load-project", {"serialized-content": "%s" % content.replace("\n", "")}) f.close() def _newProjectLoadingCb(self, unused_project_manager, uri): self._setScenarioFile(uri) def _newProjectLoaded(self, unused_project_manager, project, unused_fully_loaded): self.action_log.clean() self.timeline_log_observer.startObserving(project.timeline) self.project_log_observer.startObserving(project) def _projectClosed(self, unused_project_manager, project): self.project_log_observer.stopObserving(project) self.timeline_log_observer.stopObserving(project.timeline) if self._scenario_file: self.write_action("stop") self._scenario_file.close() self._scenario_file = None def _checkVersion(self): """ Check online for release versions information. """ self.info("Requesting version information async") giofile = Gio.File.new_for_uri(RELEASES_URL) giofile.load_contents_async(None, self._versionInfoReceivedCb, None) def _versionInfoReceivedCb(self, giofile, result, user_data): try: raw = giofile.load_contents_finish(result)[1] if not isinstance(raw, str): raw = raw.decode() raw = raw.split("\n") # Split line at '=' if the line is not empty or a comment line data = [ element.split("=") for element in raw if element and not element.startswith("#") ] # search newest version and status status = "UNSUPPORTED" current_version = None for version, version_status in data: if VERSION == version: status = version_status if version_status.upper() == "CURRENT": # This is the latest. current_version = version self.info("Latest software version is %s", current_version) VERSION_split = [int(i) for i in VERSION.split(".")] current_version_split = [ int(i) for i in current_version.split(".") ] if VERSION_split > current_version_split: status = "CURRENT" self.info( "Running version %s, which is newer than the latest known version. Considering it as the latest current version.", VERSION) elif status is "UNSUPPORTED": self.warning("Using an outdated version of Pitivi (%s)", VERSION) self._version_information["current"] = current_version self._version_information["status"] = status self.emit("version-info-received", self._version_information) except Exception as e: self.warning("Version info could not be read: %s", e) def isLatest(self): """ Whether the app's version is the latest as far as we know. """ status = self._version_information.get("status") return status is None or status.upper() == "CURRENT" def getLatest(self): """ Get the latest version of the app or None. """ return self._version_information.get("current") def _quitCb(self, unused_action, unused_param): self.shutdown() def _undoCb(self, unused_action, unused_param): self.action_log.undo() def _redoCb(self, unused_action, unused_param): self.action_log.redo() def _actionLogCommit(self, action_log, unused_stack, nested): if nested: return self._syncDoUndo(action_log) def _actionLogUndo(self, action_log, unused_stack): self._syncDoUndo(action_log) def _actionLogRedo(self, action_log, unused_stack): self._syncDoUndo(action_log) def _actionLogCleaned(self, action_log): self._syncDoUndo(action_log) def _syncDoUndo(self, action_log): can_undo = bool(action_log.undo_stacks) self.undo_action.set_enabled(can_undo) can_redo = bool(action_log.redo_stacks) self.redo_action.set_enabled(can_redo) dirty = action_log.dirty() self.project_manager.current_project.setModificationState(dirty) # In the tests we do not want to create any gui if self.gui is not None: self.gui.showProjectStatus()
class Pitivi(Gtk.Application, Loggable): """ Pitivi's application. @type effects: L{EffectsManager} @ivar gui: The main window of the app. @type gui: L{PitiviMainWindow} @ivar project_manager: The project manager object used in the application @type project_manager: L{ProjectManager} @ivar settings: Application-wide settings. @type settings: L{GlobalSettings}. """ __gsignals__ = { "version-info-received": (GObject.SIGNAL_RUN_LAST, None, (object,)) } def __init__(self): Gtk.Application.__init__(self, application_id="org.pitivi", flags=Gio.ApplicationFlags.HANDLES_OPEN) Loggable.__init__(self) self.settings = None self.threads = None self.effects = None self.system = None self.project_manager = ProjectManager(self) self.action_log = UndoableActionLog(self) self.timeline_log_observer = None self.project_log_observer = None self._last_action_time = Gst.util_get_timestamp() self.gui = None self.welcome_wizard = None self._version_information = {} self._scenario_file = None self._first_action = True self.connect("startup", self._startupCb) self.connect("activate", self._activateCb) self.connect("open", self.openCb) def write_action(self, action, properties={}): if self._first_action: self._scenario_file.write( "description, seek=true, handles-states=true\n") self._first_action = False now = Gst.util_get_timestamp() if now - self._last_action_time > 0.05 * Gst.SECOND: # We need to make sure that the waiting time was more than 50 ms. st = Gst.Structure.new_empty("wait") st["duration"] = float((now - self._last_action_time) / Gst.SECOND) self._scenario_file.write(st.to_string() + "\n") self._last_action_time = now if not isinstance(action, Gst.Structure): structure = Gst.Structure.new_empty(action) for key, value in properties.items(): structure[key] = value action = structure self._scenario_file.write(action.to_string() + "\n") self._scenario_file.flush() def _startupCb(self, unused_app): # Init logging as early as possible so we can log startup code enable_color = not os.environ.get( 'PITIVI_DEBUG_NO_COLOR', '0') in ('', '1') # Let's show a human-readable Pitivi debug output by default, and only # show a crazy unreadable mess when surrounded by gst debug statements. enable_crack_output = "GST_DEBUG" in os.environ log.init('PITIVI_DEBUG', enable_color, enable_crack_output) self.info('starting up') self.settings = GlobalSettings() self.threads = ThreadMaster() self.effects = EffectsManager() self.system = getSystem() self.action_log.connect("commit", self._actionLogCommit) self.action_log.connect("undo", self._actionLogUndo) self.action_log.connect("redo", self._actionLogRedo) self.action_log.connect("cleaned", self._actionLogCleaned) self.timeline_log_observer = TimelineLogObserver(self.action_log) self.project_log_observer = ProjectLogObserver(self.action_log) self.project_manager.connect( "new-project-loading", self._newProjectLoadingCb) self.project_manager.connect( "new-project-loaded", self._newProjectLoaded) self.project_manager.connect("project-closed", self._projectClosed) self._createActions() self._checkVersion() def _createActions(self): self.undo_action = Gio.SimpleAction.new("undo", None) self.undo_action.connect("activate", self._undoCb) self.add_action(self.undo_action) self.add_accelerator("<Control>z", "app.undo", None) self.redo_action = Gio.SimpleAction.new("redo", None) self.redo_action.connect("activate", self._redoCb) self.add_action(self.redo_action) self.add_accelerator("<Control><Shift>z", "app.redo", None) self.quit_action = Gio.SimpleAction.new("quit", None) self.quit_action.connect("activate", self._quitCb) self.add_action(self.quit_action) self.add_accelerator("<Control>q", "app.quit", None) def _activateCb(self, unused_app): if self.gui: # The app is already started and the window already created. # Present the already existing window. try: # TODO: Use present() instead of present_with_time() when # https://bugzilla.gnome.org/show_bug.cgi?id=688830 is fixed. from gi.repository import GdkX11 x11_server_time = GdkX11.x11_get_server_time(self.gui.get_window()) self.gui.present_with_time(x11_server_time) except ImportError: # On Wayland or Quartz (Mac OS X) backend there is no GdkX11, # so just use present() directly here. self.gui.present() # No need to show the welcome wizard. return self.createMainWindow() self.welcome_wizard = StartUpWizard(self) self.welcome_wizard.show() def createMainWindow(self): if self.gui: return self.gui = PitiviMainWindow(self) self.add_window(self.gui) # We might as well show it. self.gui.show() def openCb(self, unused_app, giofiles, unused_count, unused_hint): assert giofiles self.createMainWindow() if len(giofiles) > 1: self.warning( "Can open only one project file at a time. Ignoring the rest!") project_file = giofiles[0] self.project_manager.loadProject(quote_uri(project_file.get_uri())) return True def shutdown(self): """ Close Pitivi. @return: C{True} if Pitivi was successfully closed, else C{False}. @rtype: C{bool} """ self.debug("shutting down") # Refuse to close if we are not done with the current project. if not self.project_manager.closeRunningProject(): self.warning( "Not closing since running project doesn't want to close") return False if self.welcome_wizard: self.welcome_wizard.hide() if self.gui: self.gui.destroy() self.threads.stopAllThreads() self.settings.storeSettings() self.quit() return True self._first_action = True def _setScenarioFile(self, uri): if 'PITIVI_SCENARIO_FILE' in os.environ: uri = quote_uri(os.environ['PITIVI_SCENARIO_FILE']) else: cache_dir = get_dir(os.path.join(xdg_cache_home(), "scenarios")) scenario_name = str(time.strftime("%Y%m%d-%H%M%S")) project_path = None if uri: project_path = path_from_uri(uri) scenario_name += os.path.splitext(project_path.replace(os.sep, "_"))[0] uri = os.path.join(cache_dir, scenario_name + ".scenario") uri = quote_uri(uri) self._scenario_file = open(path_from_uri(uri), "w") if project_path: f = open(project_path) content = f.read() if not project_path.endswith(".scenario"): self.write_action("load-project", {"serialized-content": "%s" % content.replace("\n", "")}) f.close() def _newProjectLoadingCb(self, unused_project_manager, uri): self._setScenarioFile(uri) def _newProjectLoaded(self, unused_project_manager, project, unused_fully_loaded): self.action_log.clean() self.timeline_log_observer.startObserving(project.timeline) self.project_log_observer.startObserving(project) def _projectClosed(self, unused_project_manager, project): self.project_log_observer.stopObserving(project) self.timeline_log_observer.stopObserving(project.timeline) if self._scenario_file: self.write_action("stop") self._scenario_file.close() self._scenario_file = None def _checkVersion(self): """ Check online for release versions information. """ self.info("Requesting version information async") giofile = Gio.File.new_for_uri(RELEASES_URL) giofile.load_contents_async(None, self._versionInfoReceivedCb, None) def _versionInfoReceivedCb(self, giofile, result, user_data): try: raw = giofile.load_contents_finish(result)[1] if not isinstance(raw, str): raw = raw.decode() raw = raw.split("\n") # Split line at '=' if the line is not empty or a comment line data = [element.split("=") for element in raw if element and not element.startswith("#")] # search newest version and status status = "UNSUPPORTED" current_version = None for version, version_status in data: if VERSION == version: status = version_status if version_status.upper() == "CURRENT": # This is the latest. current_version = version self.info("Latest software version is %s", current_version) VERSION_split = [int(i) for i in VERSION.split(".")] current_version_split = [int(i) for i in current_version.split(".")] if VERSION_split > current_version_split: status = "CURRENT" self.info( "Running version %s, which is newer than the latest known version. Considering it as the latest current version.", VERSION) elif status is "UNSUPPORTED": self.warning( "Using an outdated version of Pitivi (%s)", VERSION) self._version_information["current"] = current_version self._version_information["status"] = status self.emit("version-info-received", self._version_information) except Exception as e: self.warning("Version info could not be read: %s", e) def isLatest(self): """ Whether the app's version is the latest as far as we know. """ status = self._version_information.get("status") return status is None or status.upper() == "CURRENT" def getLatest(self): """ Get the latest version of the app or None. """ return self._version_information.get("current") def _quitCb(self, unused_action, unused_param): self.shutdown() def _undoCb(self, unused_action, unused_param): self.action_log.undo() def _redoCb(self, unused_action, unused_param): self.action_log.redo() def _actionLogCommit(self, action_log, unused_stack, nested): if nested: return self._syncDoUndo(action_log) def _actionLogUndo(self, action_log, unused_stack): self._syncDoUndo(action_log) def _actionLogRedo(self, action_log, unused_stack): self._syncDoUndo(action_log) def _actionLogCleaned(self, action_log): self._syncDoUndo(action_log) def _syncDoUndo(self, action_log): can_undo = bool(action_log.undo_stacks) self.undo_action.set_enabled(can_undo) can_redo = bool(action_log.redo_stacks) self.redo_action.set_enabled(can_redo) dirty = action_log.dirty() self.project_manager.current_project.setModificationState(dirty) # In the tests we do not want to create any gui if self.gui is not None: self.gui.showProjectStatus()
class TestTimelineUndo(TestCase): def setUp(self): app = Pitivi() app._startupCb(app) app.project_manager.newBlankProject() self.timeline = app.project_manager.current_project.timeline self.layer = GES.Layer() self.timeline.add_layer(self.layer) self.action_log = UndoableActionLog() self.observer = TimelineLogObserverSpy(self.action_log) self.observer.startObserving(self.timeline) def getTimelineClips(self): for layer in self.timeline.layers: for clip in layer.get_clips(): yield clip @staticmethod def commitCb(action_log, stack, nested, stacks): stacks.append(stack) def testAddClip(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.action_log.begin("add clip") self.layer.add_clip(clip1) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(3, len(stack.done_actions)) action = stack.done_actions[0] self.assertTrue(isinstance(action, ClipAdded)) self.assertTrue(clip1 in self.getTimelineClips()) self.action_log.undo() self.assertFalse(clip1 in self.getTimelineClips()) self.action_log.redo() self.assertTrue(clip1 in self.getTimelineClips()) def testRemoveClip(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.layer.add_clip(clip1) self.action_log.begin("remove clip") self.layer.remove_clip(clip1) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions)) action = stack.done_actions[0] self.assertTrue(isinstance(action, ClipRemoved)) self.assertFalse(clip1 in self.getTimelineClips()) self.action_log.undo() self.assertTrue(clip1 in self.getTimelineClips()) self.action_log.redo() self.assertFalse(clip1 in self.getTimelineClips()) def testAddEffectToClip(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.layer.add_clip(clip1) effect1 = GES.Effect.new("agingtv") self.action_log.begin("add effect") clip1.add(effect1) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions), stack.done_actions) action = stack.done_actions[0] self.assertTrue(isinstance(action, TrackElementAdded)) self.assertTrue(effect1 in clip1.get_children(True)) self.assertEqual( 1, len([ effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect) ])) self.action_log.undo() self.assertFalse(effect1 in clip1.get_children(True)) self.action_log.redo() self.assertEqual( 1, len([ effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect) ])) def testRemoveEffectFromClip(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.layer.add_clip(clip1) effect1 = GES.Effect.new("agingtv") self.action_log.begin("add effect") clip1.add(effect1) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions), stack.done_actions) action = stack.done_actions[0] self.assertTrue(isinstance(action, TrackElementAdded)) self.assertTrue(effect1 in clip1.get_children(True)) self.assertEqual( 1, len([ effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect) ])) self.action_log.begin("remove effect") clip1.remove(effect1) self.action_log.commit() self.assertEqual( 0, len([ effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect) ])) self.action_log.undo() self.assertEqual( 1, len([ effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect) ])) self.action_log.redo() self.assertEqual( 0, len([ effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect) ])) def testChangeEffectProperty(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.layer.add_clip(clip1) effect1 = GES.Effect.new("agingtv") self.action_log.begin("add effect") clip1.add(effect1) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions), stack.done_actions) action = stack.done_actions[0] self.assertTrue(isinstance(action, TrackElementAdded)) self.assertTrue(effect1 in clip1.get_children(True)) self.assertEqual( 1, len([ effect for effect in clip1.get_children(True) if isinstance(effect, GES.Effect) ])) self.action_log.begin("change child property") effect1.set_child_property("scratch-lines", 0) self.action_log.commit() self.assertEqual(effect1.get_child_property("scratch-lines")[1], 0) self.action_log.undo() self.assertEqual(effect1.get_child_property("scratch-lines")[1], 7) self.action_log.redo() self.assertEqual(effect1.get_child_property("scratch-lines")[1], 0) self.action_log.undo() self.assertTrue(effect1 in clip1.get_children(True)) self.action_log.undo() self.assertFalse(effect1 in clip1.get_children(True)) def testClipPropertyChange(self): stacks = [] self.action_log.connect("commit", TestTimelineUndo.commitCb, stacks) clip1 = GES.TitleClip() self.layer.add_clip(clip1) clip1.set_start(5 * Gst.SECOND) clip1.set_duration(20 * Gst.SECOND) self.layer.add_clip(clip1) self.action_log.begin("modify clip") clip1.set_start(10 * Gst.SECOND) self.action_log.commit() self.assertEqual(1, len(stacks)) stack = stacks[0] self.assertEqual(1, len(stack.done_actions)) action = stack.done_actions[0] self.assertTrue(isinstance(action, ClipPropertyChanged)) self.assertEqual(10 * Gst.SECOND, clip1.get_start()) self.action_log.undo() self.assertEqual(5 * Gst.SECOND, clip1.get_start()) self.action_log.redo() self.assertEqual(10 * Gst.SECOND, clip1.get_start()) clip1.set_priority(10) self.action_log.begin("priority change") clip1.set_priority(20) self.action_log.commit() self.assertEqual(20, clip1.get_priority()) self.action_log.undo() self.assertEqual(10, clip1.get_priority()) self.action_log.redo() self.assertEqual(20, clip1.get_priority()) def testUngroup(self): uri = common.TestCase.getSampleUri("tears_of_steel.webm") asset = GES.UriClipAsset.request_sync(uri) clip1 = asset.extract() self.layer.add_clip(clip1) clip1.set_start(5 * Gst.SECOND) clip1.set_duration(0.5 * Gst.SECOND) timeline_clips = list(self.getTimelineClips()) self.assertEqual(1, len(timeline_clips), timeline_clips) self.assertEqual(5 * Gst.SECOND, timeline_clips[0].get_start()) self.assertEqual(0.5 * Gst.SECOND, timeline_clips[0].get_duration()) self.action_log.begin("ungroup") ungrouped = GES.Container.ungroup(clip1, False) self.assertEqual(2, len(ungrouped), ungrouped) self.action_log.commit() timeline_clips = list(self.getTimelineClips()) self.assertEqual(2, len(timeline_clips), timeline_clips) self.assertEqual(5 * Gst.SECOND, timeline_clips[0].get_start()) self.assertEqual(0.5 * Gst.SECOND, timeline_clips[0].get_duration()) self.assertEqual(5 * Gst.SECOND, timeline_clips[1].get_start()) self.assertEqual(0.5 * Gst.SECOND, timeline_clips[1].get_duration()) self.action_log.undo() timeline_clips = list(self.getTimelineClips()) self.assertEqual(1, len(timeline_clips)) self.assertEqual(5 * Gst.SECOND, timeline_clips[0].get_start()) self.assertEqual(0.5 * Gst.SECOND, timeline_clips[0].get_duration())
class Pitivi(Gtk.Application, Loggable): """ Pitivi's application. @type effects: L{EffectsManager} @ivar gui: The main window of the app. @type gui: L{PitiviMainWindow} @ivar project_manager: The project manager object used in the application @type project_manager: L{ProjectManager} @ivar settings: Application-wide settings. @type settings: L{GlobalSettings}. """ __gsignals__ = { "version-info-received": (GObject.SIGNAL_RUN_LAST, None, (object,)) } def __init__(self): Gtk.Application.__init__(self, application_id="org.pitivi", flags=Gio.ApplicationFlags.HANDLES_OPEN) Loggable.__init__(self) self.settings = None self.threads = None self.effects = None self.system = None self.project_manager = ProjectManager(self) self.action_log = UndoableActionLog() self.timeline_log_observer = None self.project_log_observer = None self.gui = None self.welcome_wizard = None self._version_information = {} self.connect("startup", self._startupCb) self.connect("activate", self._activateCb) self.connect("open", self.openCb) def _startupCb(self, unused_app): # Init logging as early as possible so we can log startup code enable_color = not os.environ.get('PITIVI_DEBUG_NO_COLOR', '0') in ('', '1') # Let's show a human-readable Pitivi debug output by default, and only # show a crazy unreadable mess when surrounded by gst debug statements. enable_crack_output = "GST_DEBUG" in os.environ log.init('PITIVI_DEBUG', enable_color, enable_crack_output) self.info('starting up') self.settings = GlobalSettings() self.threads = ThreadMaster() self.effects = EffectsManager() self.system = getSystem() self.action_log.connect("commit", self._actionLogCommit) self.action_log.connect("undo", self._actionLogUndo) self.action_log.connect("redo", self._actionLogRedo) self.action_log.connect("cleaned", self._actionLogCleaned) self.timeline_log_observer = TimelineLogObserver(self.action_log) self.project_log_observer = ProjectLogObserver(self.action_log) self.project_manager.connect("new-project-loaded", self._newProjectLoaded) self.project_manager.connect("project-closed", self._projectClosed) self._createActions() self._checkVersion() def _createActions(self): self.undo_action = Gio.SimpleAction.new("undo", None) self.undo_action.connect("activate", self._undoCb) self.add_action(self.undo_action) self.add_accelerator("<Control>z", "app.undo", None) self.redo_action = Gio.SimpleAction.new("redo", None) self.redo_action.connect("activate", self._redoCb) self.add_action(self.redo_action) self.add_accelerator("<Control><Shift>z", "app.redo", None) self.quit_action = Gio.SimpleAction.new("quit", None) self.quit_action.connect("activate", self._quitCb) self.add_action(self.quit_action) self.add_accelerator("<Control>q", "app.quit", None) def _activateCb(self, unused_app): if self.gui: # The app is already started and the window already created. # Present the already existing window. # TODO: Use present() instead of present_with_time() when # https://bugzilla.gnome.org/show_bug.cgi?id=688830 is fixed. x11_server_time = GdkX11.x11_get_server_time(self.gui.get_window()) self.gui.present_with_time(x11_server_time) # No need to show the welcome wizard. return self.createMainWindow() self.welcome_wizard = StartUpWizard(self) self.welcome_wizard.show() def createMainWindow(self): if self.gui: return self.gui = PitiviMainWindow(self) self.add_window(self.gui) # We might as well show it. self.gui.show() def openCb(self, unused_app, giofiles, unused_count, unused_hint): assert giofiles self.createMainWindow() if len(giofiles) > 1: self.warning("Can open only one project file at a time. Ignoring the rest!") project_file = giofiles[0] self.project_manager.loadProject(quote_uri(project_file.get_uri())) return True def shutdown(self): """ Close Pitivi. @return: C{True} if Pitivi was successfully closed, else C{False}. @rtype: C{bool} """ self.debug("shutting down") # Refuse to close if we are not done with the current project. if not self.project_manager.closeRunningProject(): self.warning("Not closing since running project doesn't want to close") return False if self.welcome_wizard: self.welcome_wizard.hide() if self.gui: self.gui.destroy() self.threads.stopAllThreads() self.settings.storeSettings() self.quit() return True def _newProjectLoaded(self, unused_project_manager, project, unused_fully_loaded): self.action_log.clean() self.timeline_log_observer.startObserving(project.timeline) self.project_log_observer.startObserving(project) def _projectClosed(self, unused_project_manager, project): self.project_log_observer.stopObserving(project) self.timeline_log_observer.stopObserving(project.timeline) def _checkVersion(self): """ Check online for release versions information. """ self.info("Requesting version information async") giofile = Gio.File.new_for_uri(RELEASES_URL) giofile.load_contents_async(None, self._versionInfoReceivedCb, None) def _versionInfoReceivedCb(self, giofile, result, user_data): try: raw = giofile.load_contents_finish(result)[1] if not isinstance(raw, str): raw = raw.decode() raw = raw.split("\n") # Split line at '=' if the line is not empty or a comment line data = [element.split("=") for element in raw if element and not element.startswith("#")] # search newest version and status status = "UNSUPPORTED" current_version = None for version, version_status in data: if VERSION == version: status = version_status if version_status.upper() == "CURRENT": # This is the latest. current_version = version self.info("Latest software version is %s", current_version) VERSION_split = [int(i) for i in VERSION.split(".")] current_version_split = [int(i) for i in current_version.split(".")] if VERSION_split > current_version_split: status = "CURRENT" self.info("Running version %s, which is newer than the latest known version. Considering it as the latest current version.", VERSION) elif status is "UNSUPPORTED": self.warning("Using an outdated version of Pitivi (%s)", VERSION) self._version_information["current"] = current_version self._version_information["status"] = status self.emit("version-info-received", self._version_information) except Exception as e: self.warning("Version info could not be read: %s", e) def isLatest(self): """ Whether the app's version is the latest as far as we know. """ status = self._version_information.get("status") return status is None or status.upper() == "CURRENT" def getLatest(self): """ Get the latest version of the app or None. """ return self._version_information.get("current") def _quitCb(self, unused_action, unused_param): self.shutdown() def _undoCb(self, unused_action, unused_param): self.action_log.undo() def _redoCb(self, unused_action, unused_param): self.action_log.redo() def _actionLogCommit(self, action_log, unused_stack, nested): if nested: return self._syncDoUndo(action_log) def _actionLogUndo(self, action_log, unused_stack): self._syncDoUndo(action_log) def _actionLogRedo(self, action_log, unused_stack): self._syncDoUndo(action_log) def _actionLogCleaned(self, action_log): self._syncDoUndo(action_log) def _syncDoUndo(self, action_log): can_undo = bool(action_log.undo_stacks) self.undo_action.set_enabled(can_undo) can_redo = bool(action_log.redo_stacks) self.redo_action.set_enabled(can_redo) dirty = action_log.dirty() self.project_manager.current_project.setModificationState(dirty) # In the tests we do not want to create any gui if self.gui is not None: self.gui.showProjectStatus()