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