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 import copy
33 
34 try:
35  import json
36 except ImportError:
37  import simplejson as json
38 
39 
40 ##
41 # Interface for classes that listen for 'undo' and 'redo' events.
43 
44  ##
45  # Easily be notified each time there are 'undo' or 'redo' actions available in the UpdateManager.
46  def updateStatusChanged(self, undo_status, redo_status):
47  raise NotImplementedError("updateStatus() not implemented in UpdateWatcher implementer.")
48 
49 
50 ##
51 # Interface for classes that listen for changes (insert, update, and delete).
53 
54  ##
55  # This method is invoked each time the UpdateManager is changed. The action contains all the details of what changed,
56  # including the type of change (insert, update, or delete).
57  def changed(self, action):
58  raise NotImplementedError("changed() not implemented in UpdateInterface implementer.")
59 
60 
61 ##
62 # A data structure representing a single update manager action, including any necessary data to reverse the action.
64 
65  def __init__(self, type=None, key=[], values=None, partial_update=False):
66  self.type = type # insert, update, or delete
67  self.key = key # list which contains the path to the item, for example: ["clips",{"id":"123"}]
68  self.values = values
69  self.old_values = None
70  self.partial_update = partial_update
71 
72  def set_old_values(self, old_vals):
73  self.old_values = old_vals
74 
75  ##
76  # Get the JSON string representing this UpdateAction
77  def json(self, is_array=False, only_value=False):
78 
79  # Build the dictionary to be serialized
80  if only_value:
81  data_dict = copy.deepcopy(self.values)
82  else:
83  data_dict = {"type": self.type,
84  "key": self.key,
85  "value": copy.deepcopy(self.values),
86  "partial": self.partial_update,
87  "old_values": copy.deepcopy(self.old_values)}
88 
89  # Always remove 'history' key (if found)
90  if "history" in data_dict:
91  data_dict.pop("history")
92 
93  if not is_array:
94  # Use a JSON Object as the root object
95  update_action_dict = data_dict
96  else:
97  # Use a JSON Array as the root object
98  update_action_dict = [data_dict]
99 
100  # Serialize as JSON
101  return json.dumps(update_action_dict)
102 
103  ##
104  # Load this UpdateAction from a JSON string
105  def load_json(self, value):
106 
107  # Load JSON string
108  update_action_dict = json.loads(value)
109 
110  # Set the Update Action properties
111  self.type = update_action_dict.get("type")
112  self.key = update_action_dict.get("key")
113  self.values = update_action_dict.get("value")
114  self.old_values = update_action_dict.get("old_values")
115  self.partial_update = update_action_dict.get("partial")
116 
117 
118 ##
119 # This class is used to track and distribute changes to listeners. Typically, only 1 instance of this class is needed,
120 # and many different listeners are connected with the add_listener() method.
122 
123  def __init__(self):
124  self.statusWatchers = [] # List of watchers
125  self.updateListeners = [] # List of listeners
126  self.actionHistory = [] # List of actions performed to current state
127  self.redoHistory = [] # List of actions undone
128  self.currentStatus = [None, None] # Status of Undo and Redo buttons (true/false for should be enabled)
129  self.ignore_history = False # Ignore saving actions to history, to prevent a huge undo/redo list
130  self.last_action = None
131 
132  ##
133  # Load history from project
134  def load_history(self, project):
135  self.redoHistory.clear()
136  self.actionHistory.clear()
137 
138  # Get history from project data
139  history = project.get(["history"])
140 
141  # Loop through each, and load serialized data into updateAction objects
142  for actionDict in history.get("redo", []):
143  if "history" not in actionDict.keys():
144  action = UpdateAction()
145  action.load_json(json.dumps(actionDict))
146  self.redoHistory.append(action)
147  for actionDict in history.get("undo", []):
148  if "history" not in actionDict.keys():
149  action = UpdateAction()
150  action.load_json(json.dumps(actionDict))
151  self.actionHistory.append(action)
152 
153  # Notify watchers of new status
154  self.update_watchers()
155 
156  ##
157  # Save history to project
158  def save_history(self, project, history_length):
159  redo_list = []
160  undo_list = []
161 
162  # Loop through each, and serialize
163  history_length_int = int(history_length)
164  for action in self.redoHistory[-history_length_int:]:
165  redo_list.append(json.loads(action.json()))
166  for action in self.actionHistory[-history_length_int:]:
167  undo_list.append(json.loads(action.json()))
168 
169  # Set history data in project
170  self.ignore_history = True
171  self.update(["history"], { "redo": redo_list, "undo": undo_list})
172  self.ignore_history = False
173 
174  ##
175  # Reset the UpdateManager, and clear all UpdateActions and History. This does not clear listeners and watchers.
176  def reset(self):
177  self.actionHistory.clear()
178  self.redoHistory.clear()
179 
180  ##
181  # Add a new listener (which will invoke the changed(action) method each time an UpdateAction is available).
182  def add_listener(self, listener, index=-1):
183 
184  if not listener in self.updateListeners:
185  if index <= -1:
186  # Add listener to end of list
187  self.updateListeners.append(listener)
188  else:
189  # Insert listener at index
190  self.updateListeners.insert(index, listener)
191  else:
192  log.warning("Listener already added.")
193 
194  ##
195  # Add a new watcher (which will invoke the updateStatusChanged() method each time a 'redo' or 'undo' action is available).
196  def add_watcher(self, watcher):
197 
198  if not watcher in self.statusWatchers:
199  self.statusWatchers.append(watcher)
200  else:
201  log.warning("Watcher already added.")
202 
203  ##
204  # Notify all watchers if any 'undo' or 'redo' actions are available.
205  def update_watchers(self):
206 
207  new_status = (len(self.actionHistory) >= 1, len(self.redoHistory) >= 1)
208  if self.currentStatus[0] != new_status[0] or self.currentStatus[1] != new_status[1]:
209  for watcher in self.statusWatchers:
210  watcher.updateStatusChanged(*new_status)
211 
212  # This can only be called on actions already run,
213  # as the old_values member is only populated during the
214  # add/update/remove task on the project data store.
215  # the old_values member is needed to reverse the changes
216  # caused by actions.
217  ##
218  # Convert an UpdateAction into the opposite type (i.e. 'insert' becomes an 'delete')
219  def get_reverse_action(self, action):
220 
221  # log.info("Reversing action: {}, {}, {}, {}".format(action.type, action.key, action.values, action.partial_update))
222  reverse = UpdateAction(action.type, action.key, action.values, action.partial_update)
223  # On adds, setup remove
224  if action.type == "insert":
225  reverse.type = "delete"
226 
227  # replace last part of key with ID (so the delete knows which item to delete)
228  id = action.values["id"]
229  action.key.append({"id": id})
230 
231  # On removes, setup add with old value
232  elif action.type == "delete":
233  reverse.type = "insert"
234  # Remove last item from key (usually the id of the inserted item)
235  if reverse.type == "insert" and isinstance(reverse.key[-1], dict) and "id" in reverse.key[-1]:
236  reverse.key = reverse.key[:-1]
237 
238  # On updates, just swap the old and new values data
239  # Swap old and new values
240  reverse.old_values = action.values
241  reverse.values = action.old_values
242 
243  # log.info("Reversed values: {}, {}, {}, {}".format(reverse.type, reverse.key, reverse.values, reverse.partial_update))
244  return reverse
245 
246  ##
247  # Undo the last UpdateAction (and notify all listeners and watchers)
248  def undo(self):
249 
250  if len(self.actionHistory) > 0:
251  # Get last action from history (remove)
252  last_action = copy.deepcopy(self.actionHistory.pop())
253 
254  self.redoHistory.append(last_action)
255  # Get reverse of last action and perform it
256  reverse_action = self.get_reverse_action(last_action)
257  self.dispatch_action(reverse_action)
258 
259  ##
260  # Redo the last UpdateAction (and notify all listeners and watchers)
261  def redo(self):
262 
263  if len(self.redoHistory) > 0:
264  # Get last undone action off redo history (remove)
265  next_action = copy.deepcopy(self.redoHistory.pop())
266 
267  # Remove ID from insert (if found)
268  if next_action.type == "insert" and isinstance(next_action.key[-1], dict) and "id" in next_action.key[-1]:
269  next_action.key = next_action.key[:-1]
270 
271  self.actionHistory.append(next_action)
272  # Perform next redo action
273  self.dispatch_action(next_action)
274 
275  # Carry out an action on all listeners
276  ##
277  # Distribute changes to all listeners (by calling their changed() method)
278  def dispatch_action(self, action):
279 
280  try:
281  # Loop through all listeners
282  for listener in self.updateListeners:
283  # Invoke change method on listener
284  listener.changed(action)
285 
286  except Exception as ex:
287  log.error("Couldn't apply '{}' to update listener: {}\n{}".format(action.type, listener, ex))
288  self.update_watchers()
289 
290  # Perform load action (loading all project data), clearing history for taking a new path
291  ##
292  # Load all project data via an UpdateAction into the UpdateManager (this action will then be distributed to all listeners)
293  def load(self, values):
294 
295  self.last_action = UpdateAction('load', '', values)
296  self.redoHistory.clear()
297  self.dispatch_action(self.last_action)
298 
299  # Perform new actions, clearing redo history for taking a new path
300  ##
301  # Insert a new UpdateAction into the UpdateManager (this action will then be distributed to all listeners)
302  def insert(self, key, values):
303 
304  self.last_action = UpdateAction('insert', key, values)
305  self.redoHistory.clear()
306  if not self.ignore_history:
307  self.actionHistory.append(self.last_action)
308  self.dispatch_action(self.last_action)
309 
310  ##
311  # Update the UpdateManager with an UpdateAction (this action will then be distributed to all listeners)
312  def update(self, key, values, partial_update=False):
313 
314  self.last_action = UpdateAction('update', key, values, partial_update)
315  self.redoHistory.clear()
316  if not self.ignore_history:
317  self.actionHistory.append(self.last_action)
318  self.dispatch_action(self.last_action)
319 
320  ##
321  # Delete an item from the UpdateManager with an UpdateAction (this action will then be distributed to all listeners)
322  def delete(self, key):
323 
324  self.last_action = UpdateAction('delete', key)
325  self.redoHistory.clear()
326  if not self.ignore_history:
327  self.actionHistory.append(self.last_action)
328  self.dispatch_action(self.last_action)
329 
330  ##
331  # Apply the last action to the history
332  def apply_last_action_to_history(self, previous_value):
333  if self.last_action:
334  self.last_action.set_old_values(previous_value)
335  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:302
def apply_last_action_to_history
Apply the last action to the history.
Definition: updates.py:332
This class is used to track and distribute changes to listeners.
Definition: updates.py:121
def add_watcher
Add a new watcher (which will invoke the updateStatusChanged() method each time a 'redo' or 'undo' ac...
Definition: updates.py:196
def save_history
Save history to project.
Definition: updates.py:158
def json
Get the JSON string representing this UpdateAction.
Definition: updates.py:77
def update
Update the UpdateManager with an UpdateAction (this action will then be distributed to all listeners)...
Definition: updates.py:312
def load_json
Load this UpdateAction from a JSON string.
Definition: updates.py:105
def add_listener
Add a new listener (which will invoke the changed(action) method each time an UpdateAction is availab...
Definition: updates.py:182
def dispatch_action
Distribute changes to all listeners (by calling their changed() method)
Definition: updates.py:278
A data structure representing a single update manager action, including any necessary data to reverse...
Definition: updates.py:63
def load_history
Load history from project.
Definition: updates.py:134
def delete
Delete an item from the UpdateManager with an UpdateAction (this action will then be distributed to a...
Definition: updates.py:322
def reset
Reset the UpdateManager, and clear all UpdateActions and History.
Definition: updates.py:176
def redo
Redo the last UpdateAction (and notify all listeners and watchers)
Definition: updates.py:261
def load
Load all project data via an UpdateAction into the UpdateManager (this action will then be distribute...
Definition: updates.py:293
Interface for classes that listen for 'undo' and 'redo' events.
Definition: updates.py:42
def get_reverse_action
Convert an UpdateAction into the opposite type (i.e.
Definition: updates.py:219
def updateStatusChanged
Easily be notified each time there are 'undo' or 'redo' actions available in the UpdateManager.
Definition: updates.py:46
def update_watchers
Notify all watchers if any 'undo' or 'redo' actions are available.
Definition: updates.py:205
def undo
Undo the last UpdateAction (and notify all listeners and watchers)
Definition: updates.py:248
def changed
This method is invoked each time the UpdateManager is changed.
Definition: updates.py:57
Interface for classes that listen for changes (insert, update, and delete).
Definition: updates.py:52