OpenShot Video Editor  2.0.0
updates.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file contains the classes needed for tracking updates and distributing changes
5 # @author Noah Figg <eggmunkee@hotmail.com>
6 # @author Jonathan Thomas <jonathan@openshot.org>
7 # @author Olivier Girard <eolinwen@gmail.com>
8 #
9 # @section LICENSE
10 #
11 # Copyright (c) 2008-2018 OpenShot Studios, LLC
12 # (http://www.openshotstudios.com). This file is part of
13 # OpenShot Video Editor (http://www.openshot.org), an open-source project
14 # dedicated to delivering high quality video editing and animation solutions
15 # to the world.
16 #
17 # OpenShot Video Editor is free software: you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation, either version 3 of the License, or
20 # (at your option) any later version.
21 #
22 # OpenShot Video Editor is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
26 #
27 # You should have received a copy of the GNU General Public License
28 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
29 #
30 
31 from classes.logger import log
32 from classes import info
33 import copy
34 import os
35 
36 try:
37  import json
38 except ImportError:
39  import simplejson as json
40 
41 
42 ##
43 # Interface for classes that listen for 'undo' and 'redo' events.
45 
46  ##
47  # Easily be notified each time there are 'undo' or 'redo' actions available in the UpdateManager.
48  def updateStatusChanged(self, undo_status, redo_status):
49  raise NotImplementedError("updateStatus() not implemented in UpdateWatcher implementer.")
50 
51 
52 ##
53 # Interface for classes that listen for changes (insert, update, and delete).
55 
56  ##
57  # This method is invoked each time the UpdateManager is changed. The action contains all the details of what changed,
58  # including the type of change (insert, update, or delete).
59  def changed(self, action):
60  raise NotImplementedError("changed() not implemented in UpdateInterface implementer.")
61 
62 
63 ##
64 # A data structure representing a single update manager action, including any necessary data to reverse the action.
66 
67  def __init__(self, type=None, key=[], values=None, partial_update=False):
68  self.type = type # insert, update, or delete
69  self.key = key # list which contains the path to the item, for example: ["clips",{"id":"123"}]
70  self.values = values
71  self.old_values = None
72  self.partial_update = partial_update
73 
74  def set_old_values(self, old_vals):
75  self.old_values = old_vals
76 
77  ##
78  # Get the JSON string representing this UpdateAction
79  def json(self, is_array=False, only_value=False):
80 
81  # Build the dictionary to be serialized
82  if only_value:
83  data_dict = copy.deepcopy(self.values)
84  else:
85  data_dict = {"type": self.type,
86  "key": self.key,
87  "value": copy.deepcopy(self.values),
88  "partial": self.partial_update,
89  "old_values": copy.deepcopy(self.old_values)}
90 
91  # Always remove 'history' key (if found). This prevents nested "history"
92  # attributes when a project dict is loaded.
93  try:
94  if data_dict.get("value") and "history" in data_dict.get("value"):
95  data_dict.get("value").pop("history", None)
96  if data_dict.get("old_values") and "history" in data_dict.get("old_values"):
97  data_dict.get("old_values").pop("history", None)
98  except Exception:
99  log.info('Warning: failed to clear history attribute from undo/redo data.')
100 
101  if not is_array:
102  # Use a JSON Object as the root object
103  update_action_dict = data_dict
104  else:
105  # Use a JSON Array as the root object
106  update_action_dict = [data_dict]
107 
108  # Serialize as JSON
109  return json.dumps(update_action_dict)
110 
111  ##
112  # Load this UpdateAction from a JSON string
113  def load_json(self, value):
114 
115  # Load JSON string
116  update_action_dict = json.loads(value, strict=False)
117 
118  # Set the Update Action properties
119  self.type = update_action_dict.get("type")
120  self.key = update_action_dict.get("key")
121  self.values = update_action_dict.get("value")
122  self.old_values = update_action_dict.get("old_values")
123  self.partial_update = update_action_dict.get("partial")
124 
125  # Always remove 'history' key (if found). This prevents nested "history"
126  # attributes when a project dict is loaded.
127  try:
128  if self.values and "history" in self.values:
129  self.values.pop("history", None)
130  if self.old_values and "history" in self.old_values:
131  self.old_values.pop("history", None)
132  except Exception:
133  log.info('Warning: failed to clear history attribute from undo/redo data.')
134 
135 
136 ##
137 # This class is used to track and distribute changes to listeners. Typically, only 1 instance of this class is needed,
138 # and many different listeners are connected with the add_listener() method.
140 
141  def __init__(self):
142  self.statusWatchers = [] # List of watchers
143  self.updateListeners = [] # List of listeners
144  self.actionHistory = [] # List of actions performed to current state
145  self.redoHistory = [] # List of actions undone
146  self.currentStatus = [None, None] # Status of Undo and Redo buttons (true/false for should be enabled)
147  self.ignore_history = False # Ignore saving actions to history, to prevent a huge undo/redo list
148  self.last_action = None
149 
150  ##
151  # Load history from project
152  def load_history(self, project):
153  self.redoHistory.clear()
154  self.actionHistory.clear()
155 
156  # Get history from project data
157  history = project.get(["history"])
158 
159  # Loop through each, and load serialized data into updateAction objects
160  # Ignore any load actions or history update actions
161  for actionDict in history.get("redo", []):
162  action = UpdateAction()
163  action.load_json(json.dumps(actionDict))
164  if action.type != "load" and action.key[0] != "history":
165  self.redoHistory.append(action)
166  else:
167  log.info("Loading redo history, skipped key: %s" % str(action.key))
168  for actionDict in history.get("undo", []):
169  action = UpdateAction()
170  action.load_json(json.dumps(actionDict))
171  if action.type != "load" and action.key[0] != "history":
172  self.actionHistory.append(action)
173  else:
174  log.info("Loading undo history, skipped key: %s" % str(action.key))
175 
176  # Notify watchers of new status
177  self.update_watchers()
178 
179  ##
180  # Save history to project
181  def save_history(self, project, history_length):
182  redo_list = []
183  undo_list = []
184 
185  # Loop through each updateAction object and serialize
186  # Ignore any load actions or history update actions
187  history_length_int = int(history_length)
188  for action in self.redoHistory[-history_length_int:]:
189  if action.type != "load" and action.key[0] != "history":
190  actionDict = json.loads(action.json(), strict=False)
191  redo_list.append(actionDict)
192  else:
193  log.info("Saving redo history, skipped key: %s" % str(action.key))
194  for action in self.actionHistory[-history_length_int:]:
195  if action.type != "load" and action.key[0] != "history":
196  actionDict = json.loads(action.json(), strict=False)
197  undo_list.append(actionDict)
198  else:
199  log.info("Saving undo, skipped key: %s" % str(action.key))
200 
201  # Set history data in project
202  self.ignore_history = True
203  self.update(["history"], { "redo": redo_list, "undo": undo_list})
204  self.ignore_history = False
205 
206  ##
207  # Reset the UpdateManager, and clear all UpdateActions and History. This does not clear listeners and watchers.
208  def reset(self):
209  self.actionHistory.clear()
210  self.redoHistory.clear()
211 
212  ##
213  # Add a new listener (which will invoke the changed(action) method each time an UpdateAction is available).
214  def add_listener(self, listener, index=-1):
215 
216  if not listener in self.updateListeners:
217  if index <= -1:
218  # Add listener to end of list
219  self.updateListeners.append(listener)
220  else:
221  # Insert listener at index
222  self.updateListeners.insert(index, listener)
223  else:
224  log.warning("Listener already added.")
225 
226  ##
227  # Add a new watcher (which will invoke the updateStatusChanged() method each time a 'redo' or 'undo' action is available).
228  def add_watcher(self, watcher):
229 
230  if not watcher in self.statusWatchers:
231  self.statusWatchers.append(watcher)
232  else:
233  log.warning("Watcher already added.")
234 
235  ##
236  # Notify all watchers if any 'undo' or 'redo' actions are available.
237  def update_watchers(self):
238 
239  new_status = (len(self.actionHistory) >= 1, len(self.redoHistory) >= 1)
240  if self.currentStatus[0] != new_status[0] or self.currentStatus[1] != new_status[1]:
241  for watcher in self.statusWatchers:
242  watcher.updateStatusChanged(*new_status)
243 
244  # This can only be called on actions already run,
245  # as the old_values member is only populated during the
246  # add/update/remove task on the project data store.
247  # the old_values member is needed to reverse the changes
248  # caused by actions.
249  ##
250  # Convert an UpdateAction into the opposite type (i.e. 'insert' becomes an 'delete')
251  def get_reverse_action(self, action):
252  reverse = UpdateAction(action.type, action.key, action.values, action.partial_update)
253  # On adds, setup remove
254  if action.type == "insert":
255  reverse.type = "delete"
256 
257  # replace last part of key with ID (so the delete knows which item to delete)
258  id = action.values["id"]
259  action.key.append({"id": id})
260 
261  # On removes, setup add with old value
262  elif action.type == "delete":
263  reverse.type = "insert"
264  # Remove last item from key (usually the id of the inserted item)
265  if reverse.type == "insert" and isinstance(reverse.key[-1], dict) and "id" in reverse.key[-1]:
266  reverse.key = reverse.key[:-1]
267 
268  # On updates, just swap the old and new values data
269  # Swap old and new values
270  reverse.old_values = action.values
271  reverse.values = action.old_values
272 
273  return reverse
274 
275  ##
276  # Undo the last UpdateAction (and notify all listeners and watchers)
277  def undo(self):
278 
279  if len(self.actionHistory) > 0:
280  # Get last action from history (remove)
281  last_action = copy.deepcopy(self.actionHistory.pop())
282 
283  self.redoHistory.append(last_action)
284  # Get reverse of last action and perform it
285  reverse_action = self.get_reverse_action(last_action)
286  self.dispatch_action(reverse_action)
287 
288  ##
289  # Redo the last UpdateAction (and notify all listeners and watchers)
290  def redo(self):
291 
292  if len(self.redoHistory) > 0:
293  # Get last undone action off redo history (remove)
294  next_action = copy.deepcopy(self.redoHistory.pop())
295 
296  # Remove ID from insert (if found)
297  if next_action.type == "insert" and isinstance(next_action.key[-1], dict) and "id" in next_action.key[-1]:
298  next_action.key = next_action.key[:-1]
299 
300  self.actionHistory.append(next_action)
301  # Perform next redo action
302  self.dispatch_action(next_action)
303 
304  # Carry out an action on all listeners
305  ##
306  # Distribute changes to all listeners (by calling their changed() method)
307  def dispatch_action(self, action):
308 
309  try:
310  # Loop through all listeners
311  for listener in self.updateListeners:
312  # Invoke change method on listener
313  listener.changed(action)
314 
315  except Exception as ex:
316  log.error("Couldn't apply '{}' to update listener: {}\n{}".format(action.type, listener, ex))
317  self.update_watchers()
318 
319  # Perform load action (loading all project data), clearing history for taking a new path
320  ##
321  # Load all project data via an UpdateAction into the UpdateManager (this action will then be distributed to all listeners)
322  def load(self, values):
323 
324  self.last_action = UpdateAction('load', '', values)
325  self.redoHistory.clear()
326  self.actionHistory.clear()
327  self.dispatch_action(self.last_action)
328 
329  # Perform new actions, clearing redo history for taking a new path
330  ##
331  # Insert a new UpdateAction into the UpdateManager (this action will then be distributed to all listeners)
332  def insert(self, key, values):
333 
334  self.last_action = UpdateAction('insert', key, values)
335  self.redoHistory.clear()
336  if not self.ignore_history:
337  self.actionHistory.append(self.last_action)
338  self.dispatch_action(self.last_action)
339 
340  ##
341  # Update the UpdateManager with an UpdateAction (this action will then be distributed to all listeners)
342  def update(self, key, values, partial_update=False):
343 
344  self.last_action = UpdateAction('update', key, values, partial_update)
345  if self.last_action.key and self.last_action.key[0] != "history":
346  # Clear redo history for any update except a "history" update
347  self.redoHistory.clear()
348  if not self.ignore_history:
349  self.actionHistory.append(self.last_action)
350  self.dispatch_action(self.last_action)
351 
352  ##
353  # Delete an item from the UpdateManager with an UpdateAction (this action will then be distributed to all listeners)
354  def delete(self, key):
355 
356  self.last_action = UpdateAction('delete', key)
357  self.redoHistory.clear()
358  if not self.ignore_history:
359  self.actionHistory.append(self.last_action)
360  self.dispatch_action(self.last_action)
361 
362  ##
363  # Apply the last action to the history
364  def apply_last_action_to_history(self, previous_value):
365  if self.last_action:
366  self.last_action.set_old_values(previous_value)
367  self.actionHistory.append(self.last_action)
def insert
Insert a new UpdateAction into the UpdateManager (this action will then be distributed to all listene...
Definition: updates.py:332
def apply_last_action_to_history
Apply the last action to the history.
Definition: updates.py:364
This class is used to track and distribute changes to listeners.
Definition: updates.py:139
def add_watcher
Add a new watcher (which will invoke the updateStatusChanged() method each time a 'redo' or 'undo' ac...
Definition: updates.py:228
def save_history
Save history to project.
Definition: updates.py:181
def json
Get the JSON string representing this UpdateAction.
Definition: updates.py:79
def update
Update the UpdateManager with an UpdateAction (this action will then be distributed to all listeners)...
Definition: updates.py:342
def load_json
Load this UpdateAction from a JSON string.
Definition: updates.py:113
def add_listener
Add a new listener (which will invoke the changed(action) method each time an UpdateAction is availab...
Definition: updates.py:214
def dispatch_action
Distribute changes to all listeners (by calling their changed() method)
Definition: updates.py:307
A data structure representing a single update manager action, including any necessary data to reverse...
Definition: updates.py:65
def load_history
Load history from project.
Definition: updates.py:152
def delete
Delete an item from the UpdateManager with an UpdateAction (this action will then be distributed to a...
Definition: updates.py:354
def reset
Reset the UpdateManager, and clear all UpdateActions and History.
Definition: updates.py:208
def redo
Redo the last UpdateAction (and notify all listeners and watchers)
Definition: updates.py:290
def load
Load all project data via an UpdateAction into the UpdateManager (this action will then be distribute...
Definition: updates.py:322
Interface for classes that listen for 'undo' and 'redo' events.
Definition: updates.py:44
def get_reverse_action
Convert an UpdateAction into the opposite type (i.e.
Definition: updates.py:251
def updateStatusChanged
Easily be notified each time there are 'undo' or 'redo' actions available in the UpdateManager.
Definition: updates.py:48
def update_watchers
Notify all watchers if any 'undo' or 'redo' actions are available.
Definition: updates.py:237
def undo
Undo the last UpdateAction (and notify all listeners and watchers)
Definition: updates.py:277
def changed
This method is invoked each time the UpdateManager is changed.
Definition: updates.py:59
Interface for classes that listen for changes (insert, update, and delete).
Definition: updates.py:54