Skip to content

Commit

Permalink
Hybrid history (states/operations) that kinda works
Browse files Browse the repository at this point in the history
still issues with redoing states, and untested state yeeter
  • Loading branch information
maoschanz committed Jul 7, 2020
1 parent 92576c7 commit 6acaf10
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 58 deletions.
111 changes: 82 additions & 29 deletions src/history_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ def __init__(self, image, **kwargs):
def get_saved(self):
return self._is_saved

def set_initial_state(self, toolless_operation):
self._initial_operation = toolless_operation

############################################################################
# Controls accessed by DrImage #############################################

Expand All @@ -43,41 +46,41 @@ def try_undo(self):
if len(self._undo_history) > 0:
last_op = self._undo_history.pop()
self._redo_history.append(last_op)
# If the destacked operation is just a saved pixbuf, undo again
if last_op['tool_id'] is None:
# L'odeur de la récursion
return self.try_undo()
self._rebuild_from_history()

def try_redo(self, *args):
operation = self._redo_history.pop()
if operation['tool_id'] is None:
# L'odeur de la récursion
self.try_redo()
self._undo_history.append(operation)
self._image.restore_first_pixbuf() # FIXME FIXME ça marche pô
else:
self._get_tool(operation['tool_id']).apply_operation(operation)

def can_undo(self):
# XXX never called while an operation is ongoing so that's stupid
return (len(self._undo_history) != 0) or self._operation_is_ongoing()
# XXX incorrect si ya des states ???
return (len(self._undo_history) > 0) or self._operation_is_ongoing()
# XXX never called while an operation is ongoing so that ^ is stupid

def can_redo(self):
return len(self._redo_history) != 0

def update_history_actions_labels(self):
undoable_action = self._undo_history[-1:]
redoable_action = self._redo_history[-1:]
undo_label = None
redo_label = None
# TODO store/get translatable labels instead of tool_ids (issue #42)
if self._operation_is_ongoing():
# XXX pointless: the method is called at application of the operation
undo_label = self._image.active_tool().tool_id
elif len(undoable_action) > 0:
undo_label = undoable_action[0]['tool_id']
if len(redoable_action) > 0:
redo_label = redoable_action[0]['tool_id']
self._image.window.update_history_actions_labels(undo_label, redo_label)
# XXX incorrect si ya des states ???
return len(self._redo_history) > 0

# def update_history_actions_labels(self):
# """...""" # l'appel est commenté aussi
# undoable_action = self._undo_history[-1:]
# redoable_action = self._redo_history[-1:]
# undo_label = None
# redo_label = None
# # TODO store/get translatable labels instead of tool_ids (issue #42)
# # XXX doesn't work with pixbuf states anyway
# if self._operation_is_ongoing():
# # XXX pointless: the method is called after applying the operation
# undo_label = self._image.active_tool().tool_id
# elif len(undoable_action) > 0:
# undo_label = undoable_action[0]['tool_id']
# if len(redoable_action) > 0:
# redo_label = redoable_action[0]['tool_id']
# self._image.window.update_history_actions_labels(undo_label, redo_label)

############################################################################
# Serialized operations ####################################################
Expand All @@ -95,22 +98,71 @@ def add_operation(self, operation):
############################################################################
# Cached pixbufs ###########################################################

def add_save(self, pixbuf):
pass
def add_state(self, pixbuf):
if pixbuf is None:
raise Exception("Attempt to save an invalid state")
self._undo_history.append({
'tool_id': None,
'pixbuf': pixbuf,
'width': pixbuf.get_width(),
'height': pixbuf.get_height()
})
self._is_saved = True

def has_initial_pixbuf(self):
return self._initial_operation['pixbuf'] is not None

def get_last_saved_state(self):
index = self._get_last_state_index(False)
if index == -1:
return self._initial_operation
else:
return self._undo_history[index]

def _get_last_state_index(self, yeet_supernumerary_states):
"""..."""
returned_index = -1
nbPixbufs = 0
for op in self._undo_history:
if op['tool_id'] is None:
last_saved_pixbuf_op = op
returned_index = self._undo_history.index(op)
nbPixbufs += 1

# If there are too many pixbufs in the history, remove a few
if yeet_supernumerary_states and nbPixbufs > 10:
print("YEETING STATES : %s IS TOO MUCH!" % nbPixbufs)
# TODO tester cette merde là
nbPixbufs = 0
for op in self._undo_history:
if op['tool_id'] is None:
if nbPixbufs < 5:
nbPixbufs += 1
else:
op['pixbuf'] = None # XXX fuite ?
self._undo_history.remove(op)
op = {} # XXX fuite ?
# for op in self._redo_history:
# pass # TODO ça aussi non ??

print("returned_index : " + str(returned_index))
return returned_index

############################################################################
# Other private methods ####################################################

def _rebuild_from_history(self):
last_save_index = -1
"""..."""
last_save_index = self._get_last_state_index(True)
self._image.restore_first_pixbuf()
# last_save_index = self._image.restore_first_pixbuf() # TODO
history = self._undo_history.copy()
self._undo_history = []
for op in history:
if history.index(op) > last_save_index:
print("do", op['tool_id'])
self._get_tool(op['tool_id']).simple_apply_operation(op)
else:
print("skip", op['tool_id'])
self._undo_history.append(op)
self._image.update()
self._image.update_history_sensitivity()
Expand All @@ -123,10 +175,11 @@ def _get_tool(self, tool_id):
if tool_id in all_tools:
return all_tools[tool_id]
else:
# XXX throw something instead
# XXX raise something instead
self._image.window.prompt_message(True, "Error: no tool " + tool_id)

############################################################################
################################################################################



56 changes: 28 additions & 28 deletions src/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,34 +102,31 @@ def init_background(self, width, height, background_rgba):
g = float(background_rgba[1])
b = float(background_rgba[2])
a = float(background_rgba[3])
self.initial_operation = {
op = {
'tool_id': None,
'pixbuf': None,
'red': r, 'green': g, 'blue': b, 'alpha': a,
'width': width, 'height': height
}
self.init_image_common()
self._history_manager.set_initial_state(op)
self.restore_first_pixbuf()

def try_load_pixbuf(self, pixbuf):
self._load_pixbuf_common(pixbuf)
self.init_image_common()
self._load_pixbuf_common(pixbuf)
self.restore_first_pixbuf()
self.update_title()

def _new_blank_pixbuf(self, w, h):
return GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, w, h)

def get_last_saved_pixbuf_op(self):
last_saved_pixbuf_op = self.initial_operation
return last_saved_pixbuf_op

def restore_first_pixbuf(self):
"""Set the last saved pixbuf from the history as the main_pixbuf. This
is used to rebuild the picture from its history."""
last_saved_pixbuf_op = self.get_last_saved_pixbuf_op()
last_saved_pixbuf_op = self._history_manager.get_last_saved_state()

# restore the pixbuf found by get_last_saved_pixbuf_op
# restore the state found in the history
pixbuf = last_saved_pixbuf_op['pixbuf']
width = last_saved_pixbuf_op['width']
height = last_saved_pixbuf_op['height']
Expand Down Expand Up @@ -157,25 +154,29 @@ def restore_first_pixbuf(self):
def _load_pixbuf_common(self, pixbuf):
if not pixbuf.get_has_alpha():
pixbuf = pixbuf.add_alpha(False, 255, 255, 255)
# XXX garder ça en attribut fait-il encore sens ?
self.initial_operation = {
op = {
'tool_id': None,
'pixbuf': pixbuf,
'red': 0.0, 'green': 0.0, 'blue': 0.0, 'alpha': 0.0,
'width': pixbuf.get_width(), 'height': pixbuf.get_height()
}
self._history_manager.set_initial_state(op)
self.main_pixbuf = pixbuf

def reload_from_disk(self):
"""Safely reloads the image from the disk (asks if unsaved changes
should be discarded or saved)."""
if not self.window.confirm_save_modifs(): # TODO non c'est nul, l'op de
return # reload doit être annulable en plus
"""Safely reloads the image from the disk."""
if self.gfile is None:
self.window.prompt_message(True, \
_("Can't reload a never-saved file from the disk."))
return
self.add_reload_history_operation(gfile)
# TODO no, the action shouldn't be active in the first place
if not self.window.confirm_save_modifs():
self.window.prompt_message(True, \
_("Can't reload a never-saved file from the disk."))
return
disk_pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.get_file_path())
self._load_pixbuf_common(disk_pixbuf)
self.window.set_picture_title(self.update_title())
self.use_stable_pixbuf()
self.update()
self.remember_current_state()

def try_load_file(self, gfile):
self.gfile = gfile
Expand Down Expand Up @@ -265,17 +266,22 @@ def try_redo(self):
def is_saved(self):
return self._history_manager.get_saved()

def add_reload_history_operation(self, gfile): # XXX wait ça devait faire quoi ça
self.gfile = gfile
def remember_current_state(self):
self._history_manager.add_state(self.main_pixbuf.copy())

def update_history_sensitivity(self): # XXX dans le history_manager ?
def update_history_sensitivity(self):
self.set_action_sensitivity('undo', self._history_manager.can_undo())
self.set_action_sensitivity('redo', self._history_manager.can_redo())
# self.update_history_actions_labels()

def add_to_history(self, operation): # XXX ptêt simplifiable ?
def add_to_history(self, operation):
self._history_manager.add_operation(operation)

def should_replace(self):
if self._history_manager.can_undo():
return False
return not self._history_manager.has_initial_pixbuf()

############################################################################
# Misc ? ###################################################################

Expand All @@ -297,11 +303,6 @@ def update_actions_state(self):
def active_tool(self):
return self.window.active_tool()

def should_replace(self):
if not self._history_manager.can_undo():
return False
return self.initial_operation['pixbuf'] is None

############################################################################
# Drawing area, main pixbuf, and surface management ########################

Expand Down Expand Up @@ -386,7 +387,6 @@ def on_leave_image(self, *args):
self.window.set_cursor(False)

def post_save(self):
# self._is_saved = True # FIXME
self.use_stable_pixbuf()
self.update()

Expand Down
4 changes: 3 additions & 1 deletion src/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,8 @@ def _save_current_tab_to_gfile(self, gfile):
try:
pixb = self.get_active_image().main_pixbuf
utilities_save_pixbuf_to(pixb, fn, self, True)
self.get_active_image().add_reload_history_operation(gfile)
self.get_active_image().gfile = gfile
self.get_active_image().remember_current_state()
except Exception as e:
if str(e) == '2': # exception has been raised because the user wants
# to save the file under an other format (JPEG/BMP → PNG)
Expand All @@ -912,6 +913,7 @@ def _save_current_tab_to_gfile(self, gfile):
# else the exception was raised because an actual error occured, or
# the user clicked on "cancel"
self.prompt_message(False, _("Failed to save %s") % fn)
print(e)
return False
self.get_active_image().post_save()
self.set_picture_title()
Expand Down

1 comment on commit 6acaf10

@maoschanz
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.