Commit ae89189585488735cc15398f0e362c32daf7c038
1 parent
49c1f7b2
Exists in
master
and in
65 other branches
ENH: Implemented the undo and redo functionality in the edition tool
Showing
4 changed files
with
416 additions
and
16 deletions
Show diff stats
invesalius/data/mask.py
@@ -22,6 +22,7 @@ import plistlib | @@ -22,6 +22,7 @@ import plistlib | ||
22 | import random | 22 | import random |
23 | import shutil | 23 | import shutil |
24 | import tempfile | 24 | import tempfile |
25 | +import weakref | ||
25 | 26 | ||
26 | import numpy | 27 | import numpy |
27 | import vtk | 28 | import vtk |
@@ -31,12 +32,155 @@ import imagedata_utils as iu | @@ -31,12 +32,155 @@ import imagedata_utils as iu | ||
31 | 32 | ||
32 | from wx.lib.pubsub import pub as Publisher | 33 | from wx.lib.pubsub import pub as Publisher |
33 | 34 | ||
35 | + | ||
36 | +class EditionHistoryNode(object): | ||
37 | + def __init__(self, index, orientation, array, clean=False): | ||
38 | + self.index = index | ||
39 | + self.orientation = orientation | ||
40 | + self.filename = tempfile.mktemp(suffix='.npy') | ||
41 | + self.clean = clean | ||
42 | + | ||
43 | + self._save_array(array) | ||
44 | + | ||
45 | + def _save_array(self, array): | ||
46 | + numpy.save(self.filename, array) | ||
47 | + print "Saving history", self.index, self.orientation, self.filename, self.clean | ||
48 | + | ||
49 | + def commit_history(self, mvolume): | ||
50 | + array = numpy.load(self.filename) | ||
51 | + if self.orientation == 'AXIAL': | ||
52 | + mvolume[self.index+1,1:,1:] = array | ||
53 | + if self.clean: | ||
54 | + mvolume[self.index+1, 0, 0] = 1 | ||
55 | + elif self.orientation == 'CORONAL': | ||
56 | + mvolume[1:, self.index+1, 1:] = array | ||
57 | + if self.clean: | ||
58 | + mvolume[0, self.index+1, 0] = 1 | ||
59 | + elif self.orientation == 'SAGITAL': | ||
60 | + mvolume[1:, 1:, self.index+1] = array | ||
61 | + if self.clean: | ||
62 | + mvolume[0, 0, self.index+1] = 1 | ||
63 | + | ||
64 | + print "applying to", self.orientation, "at slice", self.index | ||
65 | + | ||
66 | + def __del__(self): | ||
67 | + print "Removing", self.filename | ||
68 | + os.remove(self.filename) | ||
69 | + | ||
70 | + | ||
71 | +class EditionHistory(object): | ||
72 | + def __init__(self, size=50): | ||
73 | + self.history = [] | ||
74 | + self._copies = weakref.WeakValueDictionary() | ||
75 | + self.index = -1 | ||
76 | + self.size = size | ||
77 | + | ||
78 | + Publisher.sendMessage("Enable undo", False) | ||
79 | + Publisher.sendMessage("Enable redo", False) | ||
80 | + | ||
81 | + def new_node(self, index, orientation, array, p_array, clean): | ||
82 | + try: | ||
83 | + p_node = self.history[self.index] | ||
84 | + except IndexError: | ||
85 | + p_node = None | ||
86 | + | ||
87 | + if self.index == -1 or (orientation != p_node.orientation or p_node.index != index): | ||
88 | + try: | ||
89 | + node = self._copies[(orientation, index)] | ||
90 | + except KeyError: | ||
91 | + node = EditionHistoryNode(index, orientation, p_array, clean) | ||
92 | + self._copies[(orientation, index)] = node | ||
93 | + self.add(node) | ||
94 | + | ||
95 | + node = EditionHistoryNode(index, orientation, array, clean) | ||
96 | + self._copies[(orientation, index)] = node | ||
97 | + self.add(node) | ||
98 | + | ||
99 | + def add(self, node): | ||
100 | + if self.index == self.size: | ||
101 | + self.history.pop(0) | ||
102 | + self.index -= 1 | ||
103 | + | ||
104 | + if self.index < len(self.history): | ||
105 | + self.history = self.history[:self.index + 1] | ||
106 | + self.history.append(node) | ||
107 | + self.index += 1 | ||
108 | + | ||
109 | + print "INDEX", self.index, len(self.history), self.history | ||
110 | + Publisher.sendMessage("Enable undo", True) | ||
111 | + Publisher.sendMessage("Enable redo", False) | ||
112 | + | ||
113 | + def undo(self, mvolume, actual_slices=None): | ||
114 | + h = self.history | ||
115 | + if self.index > 0: | ||
116 | + #if self.index > 0 and h[self.index].clean: | ||
117 | + ##self.index -= 1 | ||
118 | + ##h[self.index].commit_history(mvolume) | ||
119 | + #self._reload_slice(self.index - 1) | ||
120 | + if actual_slices and actual_slices[h[self.index - 1].orientation] != h[self.index - 1].index: | ||
121 | + self._reload_slice(self.index - 1) | ||
122 | + else: | ||
123 | + self.index -= 1 | ||
124 | + h[self.index].commit_history(mvolume) | ||
125 | + if actual_slices and self.index and actual_slices[h[self.index - 1].orientation] == h[self.index - 1].index: | ||
126 | + self.index -= 1 | ||
127 | + h[self.index].commit_history(mvolume) | ||
128 | + self._reload_slice(self.index) | ||
129 | + Publisher.sendMessage("Enable redo", True) | ||
130 | + | ||
131 | + if self.index == 0: | ||
132 | + Publisher.sendMessage("Enable undo", False) | ||
133 | + print "AT", self.index, len(self.history), self.history[self.index].filename | ||
134 | + | ||
135 | + def redo(self, mvolume, actual_slices=None): | ||
136 | + h = self.history | ||
137 | + if self.index < len(h) - 1: | ||
138 | + #if self.index < len(h) - 1 and h[self.index].clean: | ||
139 | + ##self.index += 1 | ||
140 | + ##h[self.index].commit_history(mvolume) | ||
141 | + #self._reload_slice(self.index + 1) | ||
142 | + | ||
143 | + if actual_slices and actual_slices[h[self.index + 1].orientation] != h[self.index + 1].index: | ||
144 | + self._reload_slice(self.index + 1) | ||
145 | + else: | ||
146 | + self.index += 1 | ||
147 | + h[self.index].commit_history(mvolume) | ||
148 | + if actual_slices and self.index < len(h) - 1 and actual_slices[h[self.index + 1].orientation] == h[self.index + 1].index: | ||
149 | + self.index += 1 | ||
150 | + h[self.index].commit_history(mvolume) | ||
151 | + self._reload_slice(self.index) | ||
152 | + Publisher.sendMessage("Enable undo", True) | ||
153 | + | ||
154 | + if self.index == len(h) - 1: | ||
155 | + Publisher.sendMessage("Enable redo", False) | ||
156 | + print "AT", self.index, len(h), h[self.index].filename | ||
157 | + | ||
158 | + def _reload_slice(self, index): | ||
159 | + Publisher.sendMessage(('Set scroll position', self.history[index].orientation), | ||
160 | + self.history[index].index) | ||
161 | + | ||
162 | + def _config_undo_redo(self, visible): | ||
163 | + v_undo = False | ||
164 | + v_redo = False | ||
165 | + | ||
166 | + if self.history and visible: | ||
167 | + v_undo = True | ||
168 | + v_redo = True | ||
169 | + if self.index == 0: | ||
170 | + v_undo = False | ||
171 | + elif self.index == len(self.history) - 1: | ||
172 | + v_redo = False | ||
173 | + | ||
174 | + Publisher.sendMessage("Enable undo", v_undo) | ||
175 | + Publisher.sendMessage("Enable redo", v_redo) | ||
176 | + | ||
177 | + | ||
34 | class Mask(): | 178 | class Mask(): |
35 | general_index = -1 | 179 | general_index = -1 |
36 | def __init__(self): | 180 | def __init__(self): |
37 | Mask.general_index += 1 | 181 | Mask.general_index += 1 |
38 | self.index = Mask.general_index | 182 | self.index = Mask.general_index |
39 | - self.imagedata = '' # vtkImageData | 183 | + self.imagedata = '' |
40 | self.colour = random.choice(const.MASK_COLOUR) | 184 | self.colour = random.choice(const.MASK_COLOUR) |
41 | self.opacity = const.MASK_OPACITY | 185 | self.opacity = const.MASK_OPACITY |
42 | self.threshold_range = const.THRESHOLD_RANGE | 186 | self.threshold_range = const.THRESHOLD_RANGE |
@@ -47,10 +191,24 @@ class Mask(): | @@ -47,10 +191,24 @@ class Mask(): | ||
47 | self.was_edited = False | 191 | self.was_edited = False |
48 | self.__bind_events() | 192 | self.__bind_events() |
49 | 193 | ||
194 | + self.history = EditionHistory() | ||
195 | + | ||
50 | def __bind_events(self): | 196 | def __bind_events(self): |
51 | Publisher.subscribe(self.OnFlipVolume, 'Flip volume') | 197 | Publisher.subscribe(self.OnFlipVolume, 'Flip volume') |
52 | Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') | 198 | Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') |
53 | 199 | ||
200 | + def save_history(self, index, orientation, array, p_array, clean=False): | ||
201 | + self.history.new_node(index, orientation, array, p_array, clean) | ||
202 | + | ||
203 | + def undo_history(self, actual_slices): | ||
204 | + self.history.undo(self.matrix, actual_slices) | ||
205 | + | ||
206 | + def redo_history(self, actual_slices): | ||
207 | + self.history.redo(self.matrix, actual_slices) | ||
208 | + | ||
209 | + def on_show(self): | ||
210 | + self.history._config_undo_redo(self.is_shown) | ||
211 | + | ||
54 | def SavePlist(self, dir_temp, filelist): | 212 | def SavePlist(self, dir_temp, filelist): |
55 | mask = {} | 213 | mask = {} |
56 | filename = u'mask_%d' % self.index | 214 | filename = u'mask_%d' % self.index |
@@ -129,3 +287,6 @@ class Mask(): | @@ -129,3 +287,6 @@ class Mask(): | ||
129 | self.temp_file = tempfile.mktemp() | 287 | self.temp_file = tempfile.mktemp() |
130 | shape = shape[0] + 1, shape[1] + 1, shape[2] + 1 | 288 | shape = shape[0] + 1, shape[1] + 1, shape[2] + 1 |
131 | self.matrix = numpy.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape) | 289 | self.matrix = numpy.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape) |
290 | + | ||
291 | + def __del__(self): | ||
292 | + self.history._config_undo_redo(False) |
invesalius/data/slice_.py
@@ -17,6 +17,8 @@ | @@ -17,6 +17,8 @@ | ||
17 | # detalhes. | 17 | # detalhes. |
18 | #-------------------------------------------------------------------------- | 18 | #-------------------------------------------------------------------------- |
19 | import math | 19 | import math |
20 | +import os | ||
21 | +import tempfile | ||
20 | 22 | ||
21 | import numpy | 23 | import numpy |
22 | import vtk | 24 | import vtk |
@@ -64,6 +66,9 @@ class SliceBuffer(object): | @@ -64,6 +66,9 @@ class SliceBuffer(object): | ||
64 | self.vtk_mask = None | 66 | self.vtk_mask = None |
65 | 67 | ||
66 | 68 | ||
69 | + | ||
70 | + | ||
71 | + | ||
67 | class Slice(object): | 72 | class Slice(object): |
68 | __metaclass__= utils.Singleton | 73 | __metaclass__= utils.Singleton |
69 | # Only one slice will be initialized per time (despite several viewers | 74 | # Only one slice will be initialized per time (despite several viewers |
@@ -135,6 +140,9 @@ class Slice(object): | @@ -135,6 +140,9 @@ class Slice(object): | ||
135 | 140 | ||
136 | Publisher.subscribe(self.OnFlipVolume, 'Flip volume') | 141 | Publisher.subscribe(self.OnFlipVolume, 'Flip volume') |
137 | Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') | 142 | Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') |
143 | + | ||
144 | + Publisher.subscribe(self.__undo_edition, 'Undo edition') | ||
145 | + Publisher.subscribe(self.__redo_edition, 'Redo edition') | ||
138 | 146 | ||
139 | def GetMaxSliceNumber(self, orientation): | 147 | def GetMaxSliceNumber(self, orientation): |
140 | shape = self.matrix.shape | 148 | shape = self.matrix.shape |
@@ -290,7 +298,6 @@ class Slice(object): | @@ -290,7 +298,6 @@ class Slice(object): | ||
290 | mask = self.buffer_slices[orientation].mask | 298 | mask = self.buffer_slices[orientation].mask |
291 | image = self.buffer_slices[orientation].image | 299 | image = self.buffer_slices[orientation].image |
292 | thresh_min, thresh_max = self.current_mask.edition_threshold_range | 300 | thresh_min, thresh_max = self.current_mask.edition_threshold_range |
293 | - self.current_mask.was_edited = True | ||
294 | 301 | ||
295 | if hasattr(position, '__iter__'): | 302 | if hasattr(position, '__iter__'): |
296 | py, px = position | 303 | py, px = position |
@@ -350,12 +357,6 @@ class Slice(object): | @@ -350,12 +357,6 @@ class Slice(object): | ||
350 | roi_m = mask[yi:yf,xi:xf] | 357 | roi_m = mask[yi:yf,xi:xf] |
351 | roi_i = image[yi:yf, xi:xf] | 358 | roi_i = image[yi:yf, xi:xf] |
352 | 359 | ||
353 | |||
354 | - print"IMAGE", roi_m.shape | ||
355 | - print "BRUSH", index.shape | ||
356 | - print "IMAGE[BRUSH]", roi_m[index].shape | ||
357 | |||
358 | - | ||
359 | if operation == const.BRUSH_THRESH: | 360 | if operation == const.BRUSH_THRESH: |
360 | # It's a trick to make points between threshold gets value 254 | 361 | # It's a trick to make points between threshold gets value 254 |
361 | # (1 * 253 + 1) and out ones gets value 1 (0 * 253 + 1). | 362 | # (1 * 253 + 1) and out ones gets value 1 (0 * 253 + 1). |
@@ -558,6 +559,8 @@ class Slice(object): | @@ -558,6 +559,8 @@ class Slice(object): | ||
558 | print "Showing Mask" | 559 | print "Showing Mask" |
559 | proj = Project() | 560 | proj = Project() |
560 | proj.mask_dict[index].is_shown = value | 561 | proj.mask_dict[index].is_shown = value |
562 | + proj.mask_dict[index].on_show() | ||
563 | + | ||
561 | if (index == self.current_mask.index): | 564 | if (index == self.current_mask.index): |
562 | for buffer_ in self.buffer_slices.values(): | 565 | for buffer_ in self.buffer_slices.values(): |
563 | buffer_.discard_vtk_mask() | 566 | buffer_.discard_vtk_mask() |
@@ -884,12 +887,37 @@ class Slice(object): | @@ -884,12 +887,37 @@ class Slice(object): | ||
884 | """ | 887 | """ |
885 | b_mask = self.buffer_slices[orientation].mask | 888 | b_mask = self.buffer_slices[orientation].mask |
886 | index = self.buffer_slices[orientation].index | 889 | index = self.buffer_slices[orientation].index |
890 | + | ||
891 | + # TODO: Voltar a usar marcacao na mascara | ||
887 | if orientation == 'AXIAL': | 892 | if orientation == 'AXIAL': |
893 | + #if self.current_mask.matrix[index+1, 0, 0] != 2: | ||
894 | + #self.current_mask.save_history(index, orientation, | ||
895 | + #self.current_mask.matrix[index+1,1:,1:], | ||
896 | + #clean=True) | ||
897 | + p_mask = self.current_mask.matrix[index+1,1:,1:].copy() | ||
888 | self.current_mask.matrix[index+1,1:,1:] = b_mask | 898 | self.current_mask.matrix[index+1,1:,1:] = b_mask |
899 | + self.current_mask.matrix[index+1, 0, 0] = 2 | ||
900 | + | ||
889 | elif orientation == 'CORONAL': | 901 | elif orientation == 'CORONAL': |
902 | + #if self.current_mask.matrix[0, index+1, 0] != 2: | ||
903 | + #self.current_mask.save_history(index, orientation, | ||
904 | + #self.current_mask.matrix[1:, index+1, 1:], | ||
905 | + #clean=True) | ||
906 | + p_mask = self.current_mask.matrix[1:, index+1, 1:].copy() | ||
890 | self.current_mask.matrix[1:, index+1, 1:] = b_mask | 907 | self.current_mask.matrix[1:, index+1, 1:] = b_mask |
908 | + self.current_mask.matrix[0, index+1, 0] = 2 | ||
909 | + | ||
891 | elif orientation == 'SAGITAL': | 910 | elif orientation == 'SAGITAL': |
911 | + #if self.current_mask.matrix[0, 0, index+1] != 2: | ||
912 | + #self.current_mask.save_history(index, orientation, | ||
913 | + #self.current_mask.matrix[1:, 1:, index+1], | ||
914 | + #clean=True) | ||
915 | + p_mask = self.current_mask.matrix[1:, 1:, index+1].copy() | ||
892 | self.current_mask.matrix[1:, 1:, index+1] = b_mask | 916 | self.current_mask.matrix[1:, 1:, index+1] = b_mask |
917 | + self.current_mask.matrix[0, 0, index+1] = 2 | ||
918 | + | ||
919 | + self.current_mask.save_history(index, orientation, b_mask, p_mask) | ||
920 | + self.current_mask.was_edited = True | ||
893 | 921 | ||
894 | for o in self.buffer_slices: | 922 | for o in self.buffer_slices: |
895 | if o != orientation: | 923 | if o != orientation: |
@@ -897,6 +925,28 @@ class Slice(object): | @@ -897,6 +925,28 @@ class Slice(object): | ||
897 | self.buffer_slices[o].discard_vtk_mask() | 925 | self.buffer_slices[o].discard_vtk_mask() |
898 | Publisher.sendMessage('Reload actual slice') | 926 | Publisher.sendMessage('Reload actual slice') |
899 | 927 | ||
928 | + def __undo_edition(self, pub_evt): | ||
929 | + buffer_slices = self.buffer_slices | ||
930 | + actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, | ||
931 | + "CORONAL": buffer_slices["CORONAL"].index, | ||
932 | + "SAGITAL": buffer_slices["SAGITAL"].index,} | ||
933 | + self.current_mask.undo_history(actual_slices) | ||
934 | + for o in self.buffer_slices: | ||
935 | + self.buffer_slices[o].discard_mask() | ||
936 | + self.buffer_slices[o].discard_vtk_mask() | ||
937 | + Publisher.sendMessage('Reload actual slice') | ||
938 | + | ||
939 | + def __redo_edition(self, pub_evt): | ||
940 | + buffer_slices = self.buffer_slices | ||
941 | + actual_slices = {"AXIAL": buffer_slices["AXIAL"].index, | ||
942 | + "CORONAL": buffer_slices["CORONAL"].index, | ||
943 | + "SAGITAL": buffer_slices["SAGITAL"].index,} | ||
944 | + self.current_mask.redo_history(actual_slices) | ||
945 | + for o in self.buffer_slices: | ||
946 | + self.buffer_slices[o].discard_mask() | ||
947 | + self.buffer_slices[o].discard_vtk_mask() | ||
948 | + Publisher.sendMessage('Reload actual slice') | ||
949 | + | ||
900 | def __build_mask(self, imagedata, create=True): | 950 | def __build_mask(self, imagedata, create=True): |
901 | # create new mask instance and insert it into project | 951 | # create new mask instance and insert it into project |
902 | if create: | 952 | if create: |
invesalius/data/viewer_slice.py
@@ -20,6 +20,7 @@ | @@ -20,6 +20,7 @@ | ||
20 | #-------------------------------------------------------------------------- | 20 | #-------------------------------------------------------------------------- |
21 | 21 | ||
22 | import itertools | 22 | import itertools |
23 | +import tempfile | ||
23 | 24 | ||
24 | import numpy | 25 | import numpy |
25 | 26 | ||
@@ -803,6 +804,7 @@ class Viewer(wx.Panel): | @@ -803,6 +804,7 @@ class Viewer(wx.Panel): | ||
803 | def OnBrushRelease(self, evt, obj): | 804 | def OnBrushRelease(self, evt, obj): |
804 | if (self.slice_.buffer_slices[self.orientation].mask is None): | 805 | if (self.slice_.buffer_slices[self.orientation].mask is None): |
805 | return | 806 | return |
807 | + | ||
806 | self.slice_.apply_slice_buffer_to_mask(self.orientation) | 808 | self.slice_.apply_slice_buffer_to_mask(self.orientation) |
807 | self._flush_buffer = False | 809 | self._flush_buffer = False |
808 | 810 |
invesalius/gui/frame.py
@@ -165,11 +165,13 @@ class Frame(wx.Frame): | @@ -165,11 +165,13 @@ class Frame(wx.Frame): | ||
165 | t2 = LayoutToolBar(self) | 165 | t2 = LayoutToolBar(self) |
166 | t3 = ObjectToolBar(self) | 166 | t3 = ObjectToolBar(self) |
167 | t4 = SliceToolBar(self) | 167 | t4 = SliceToolBar(self) |
168 | + t5 = HistoryToolBar(self) | ||
168 | else: | 169 | else: |
169 | - t4 = ProjectToolBar(self) | ||
170 | - t3 = LayoutToolBar(self) | ||
171 | - t2 = ObjectToolBar(self) | ||
172 | - t1 = SliceToolBar(self) | 170 | + t5 = ProjectToolBar(self) |
171 | + t4 = LayoutToolBar(self) | ||
172 | + t3 = ObjectToolBar(self) | ||
173 | + t2 = SliceToolBar(self) | ||
174 | + t1 = HistoryToolBar(self) | ||
173 | 175 | ||
174 | aui_manager.AddPane(t1, wx.aui.AuiPaneInfo(). | 176 | aui_manager.AddPane(t1, wx.aui.AuiPaneInfo(). |
175 | Name("General Features Toolbar"). | 177 | Name("General Features Toolbar"). |
@@ -191,6 +193,11 @@ class Frame(wx.Frame): | @@ -191,6 +193,11 @@ class Frame(wx.Frame): | ||
191 | ToolbarPane().Top().Floatable(False). | 193 | ToolbarPane().Top().Floatable(False). |
192 | LeftDockable(False).RightDockable(False)) | 194 | LeftDockable(False).RightDockable(False)) |
193 | 195 | ||
196 | + aui_manager.AddPane(t5, wx.aui.AuiPaneInfo(). | ||
197 | + Name("Slice Toolbar"). | ||
198 | + ToolbarPane().Top().Floatable(False). | ||
199 | + LeftDockable(False).RightDockable(False)) | ||
200 | + | ||
194 | aui_manager.Update() | 201 | aui_manager.Update() |
195 | self.aui_manager = aui_manager | 202 | self.aui_manager = aui_manager |
196 | 203 | ||
@@ -358,6 +365,11 @@ class Frame(wx.Frame): | @@ -358,6 +365,11 @@ class Frame(wx.Frame): | ||
358 | const.ID_SWAP_XZ: (2, 0), | 365 | const.ID_SWAP_XZ: (2, 0), |
359 | const.ID_SWAP_YZ: (1, 0)}[id] | 366 | const.ID_SWAP_YZ: (1, 0)}[id] |
360 | self.SwapAxes(axes) | 367 | self.SwapAxes(axes) |
368 | + elif id == wx.ID_UNDO: | ||
369 | + self.OnUndo() | ||
370 | + elif id == wx.ID_REDO: | ||
371 | + self.OnRedo() | ||
372 | + | ||
361 | def OnSize(self, evt): | 373 | def OnSize(self, evt): |
362 | """ | 374 | """ |
363 | Refresh GUI when frame is resized. | 375 | Refresh GUI when frame is resized. |
@@ -436,6 +448,13 @@ class Frame(wx.Frame): | @@ -436,6 +448,13 @@ class Frame(wx.Frame): | ||
436 | Publisher.sendMessage('Update scroll') | 448 | Publisher.sendMessage('Update scroll') |
437 | Publisher.sendMessage('Reload actual slice') | 449 | Publisher.sendMessage('Reload actual slice') |
438 | 450 | ||
451 | + def OnUndo(self): | ||
452 | + print "Undo" | ||
453 | + Publisher.sendMessage('Undo edition') | ||
454 | + | ||
455 | + def OnRedo(self): | ||
456 | + print "Redo" | ||
457 | + Publisher.sendMessage('Redo edition') | ||
439 | 458 | ||
440 | 459 | ||
441 | 460 | ||
@@ -476,6 +495,8 @@ class MenuBar(wx.MenuBar): | @@ -476,6 +495,8 @@ class MenuBar(wx.MenuBar): | ||
476 | # mail list in Oct 20 2008 | 495 | # mail list in Oct 20 2008 |
477 | sub = Publisher.subscribe | 496 | sub = Publisher.subscribe |
478 | sub(self.OnEnableState, "Enable state project") | 497 | sub(self.OnEnableState, "Enable state project") |
498 | + sub(self.OnEnableUndo, "Enable undo") | ||
499 | + sub(self.OnEnableRedo, "Enable redo") | ||
479 | 500 | ||
480 | def __init_items(self): | 501 | def __init_items(self): |
481 | """ | 502 | """ |
@@ -523,11 +544,10 @@ class MenuBar(wx.MenuBar): | @@ -523,11 +544,10 @@ class MenuBar(wx.MenuBar): | ||
523 | app(const.ID_SWAP_YZ, _("A-P <-> T-B")) | 544 | app(const.ID_SWAP_YZ, _("A-P <-> T-B")) |
524 | 545 | ||
525 | file_edit = wx.Menu() | 546 | file_edit = wx.Menu() |
526 | - app = file_edit.Append | ||
527 | file_edit.AppendMenu(wx.NewId(), _('Flip'), flip_menu) | 547 | file_edit.AppendMenu(wx.NewId(), _('Flip'), flip_menu) |
528 | file_edit.AppendMenu(wx.NewId(), _('Swap axes'), swap_axes_menu) | 548 | file_edit.AppendMenu(wx.NewId(), _('Swap axes'), swap_axes_menu) |
529 | - #app(wx.ID_UNDO, "Undo\tCtrl+Z") | ||
530 | - #app(wx.ID_REDO, "Redo\tCtrl+Y") | 549 | + file_edit.Append(wx.ID_UNDO, "Undo\tCtrl+Z").Enable(False) |
550 | + file_edit.Append(wx.ID_REDO, "Redo\tCtrl+Y").Enable(False) | ||
531 | #app(const.ID_EDIT_LIST, "Show Undo List...") | 551 | #app(const.ID_EDIT_LIST, "Show Undo List...") |
532 | ################################################################# | 552 | ################################################################# |
533 | 553 | ||
@@ -578,7 +598,7 @@ class MenuBar(wx.MenuBar): | @@ -578,7 +598,7 @@ class MenuBar(wx.MenuBar): | ||
578 | 598 | ||
579 | # Add all menus to menubar | 599 | # Add all menus to menubar |
580 | self.Append(file_menu, _("File")) | 600 | self.Append(file_menu, _("File")) |
581 | - #self.Append(file_edit, _("Edit")) | 601 | + self.Append(file_edit, _("Edit")) |
582 | #self.Append(view_menu, "View") | 602 | #self.Append(view_menu, "View") |
583 | #self.Append(tools_menu, "Tools") | 603 | #self.Append(tools_menu, "Tools") |
584 | self.Append(options_menu, _("Options")) | 604 | self.Append(options_menu, _("Options")) |
@@ -609,6 +629,19 @@ class MenuBar(wx.MenuBar): | @@ -609,6 +629,19 @@ class MenuBar(wx.MenuBar): | ||
609 | for item in self.enable_items: | 629 | for item in self.enable_items: |
610 | self.Enable(item, True) | 630 | self.Enable(item, True) |
611 | 631 | ||
632 | + def OnEnableUndo(self, pubsub_evt): | ||
633 | + value = pubsub_evt.data | ||
634 | + if value: | ||
635 | + self.FindItemById(wx.ID_UNDO).Enable(True) | ||
636 | + else: | ||
637 | + self.FindItemById(wx.ID_UNDO).Enable(False) | ||
638 | + | ||
639 | + def OnEnableRedo(self, pubsub_evt): | ||
640 | + value = pubsub_evt.data | ||
641 | + if value: | ||
642 | + self.FindItemById(wx.ID_REDO).Enable(True) | ||
643 | + else: | ||
644 | + self.FindItemById(wx.ID_REDO).Enable(False) | ||
612 | # ------------------------------------------------------------------ | 645 | # ------------------------------------------------------------------ |
613 | # ------------------------------------------------------------------ | 646 | # ------------------------------------------------------------------ |
614 | # ------------------------------------------------------------------ | 647 | # ------------------------------------------------------------------ |
@@ -1397,12 +1430,166 @@ class LayoutToolBar(wx.ToolBar): | @@ -1397,12 +1430,166 @@ class LayoutToolBar(wx.ToolBar): | ||
1397 | self.ontool_text = True | 1430 | self.ontool_text = True |
1398 | 1431 | ||
1399 | 1432 | ||
1433 | +class HistoryToolBar(wx.ToolBar): | ||
1434 | + """ | ||
1435 | + Toolbar related to general layout/ visualization configuration | ||
1436 | + e.g: show/hide task panel and show/hide text on viewers. | ||
1437 | + """ | ||
1438 | + def __init__(self, parent): | ||
1439 | + style = wx.TB_FLAT|wx.TB_NODIVIDER | wx.TB_DOCKABLE | ||
1440 | + wx.ToolBar.__init__(self, parent, -1, wx.DefaultPosition, | ||
1441 | + wx.DefaultSize, | ||
1442 | + style) | ||
1400 | 1443 | ||
1444 | + self.SetToolBitmapSize(wx.Size(32,32)) | ||
1401 | 1445 | ||
1446 | + self.parent = parent | ||
1447 | + self.__init_items() | ||
1448 | + self.__bind_events() | ||
1449 | + self.__bind_events_wx() | ||
1402 | 1450 | ||
1451 | + self.ontool_layout = False | ||
1452 | + self.ontool_text = True | ||
1453 | + #self.enable_items = [ID_TEXT] | ||
1403 | 1454 | ||
1455 | + self.Realize() | ||
1456 | + #self.SetStateProjectClose() | ||
1404 | 1457 | ||
1458 | + def __bind_events(self): | ||
1459 | + """ | ||
1460 | + Bind events related to pubsub. | ||
1461 | + """ | ||
1462 | + sub = Publisher.subscribe | ||
1463 | + #sub(self._EnableState, "Enable state project") | ||
1464 | + #sub(self._SetLayoutWithTask, "Set layout button data only") | ||
1465 | + #sub(self._SetLayoutWithoutTask, "Set layout button full") | ||
1466 | + sub(self.OnEnableUndo, "Enable undo") | ||
1467 | + sub(self.OnEnableRedo, "Enable redo") | ||
1405 | 1468 | ||
1469 | + def __bind_events_wx(self): | ||
1470 | + """ | ||
1471 | + Bind normal events from wx (except pubsub related). | ||
1472 | + """ | ||
1473 | + #self.Bind(wx.EVT_TOOL, self.OnToggle) | ||
1474 | + wx.EVT_TOOL( self, wx.ID_UNDO, self.OnUndo ) | ||
1475 | + wx.EVT_TOOL( self, wx.ID_REDO, self.OnRedo ) | ||
1406 | 1476 | ||
1477 | + def __init_items(self): | ||
1478 | + """ | ||
1479 | + Add tools into toolbar. | ||
1480 | + """ | ||
1481 | + self.AddSimpleTool(wx.ID_UNDO, wx.ArtProvider_GetBitmap(wx.ART_UNDO, wx.ART_OTHER, wx.Size( 16, 16)), 'Undo', '') | ||
1482 | + self.AddSimpleTool(wx.ID_REDO, wx.ArtProvider_GetBitmap(wx.ART_REDO, wx.ART_OTHER, wx.Size( 16, 16)), 'Redo', '') | ||
1483 | + self.EnableTool(wx.ID_UNDO, False) | ||
1484 | + self.EnableTool(wx.ID_REDO, False) | ||
1407 | 1485 | ||
1486 | + def _EnableState(self, pubsub_evt): | ||
1487 | + """ | ||
1488 | + Based on given state, enable or disable menu items which | ||
1489 | + depend if project is open or not. | ||
1490 | + """ | ||
1491 | + state = pubsub_evt.data | ||
1492 | + if state: | ||
1493 | + self.SetStateProjectOpen() | ||
1494 | + else: | ||
1495 | + self.SetStateProjectClose() | ||
1408 | 1496 | ||
1497 | + def _SetLayoutWithoutTask(self, pubsub_evt): | ||
1498 | + """ | ||
1499 | + Set item bitmap to task panel hiden. | ||
1500 | + """ | ||
1501 | + self.SetToolNormalBitmap(ID_LAYOUT,self.BMP_WITHOUT_MENU) | ||
1502 | + | ||
1503 | + def _SetLayoutWithTask(self, pubsub_evt): | ||
1504 | + """ | ||
1505 | + Set item bitmap to task panel shown. | ||
1506 | + """ | ||
1507 | + self.SetToolNormalBitmap(ID_LAYOUT,self.BMP_WITH_MENU) | ||
1508 | + | ||
1509 | + def OnUndo(self, event): | ||
1510 | + print "Undo" | ||
1511 | + Publisher.sendMessage('Undo edition') | ||
1512 | + | ||
1513 | + def OnRedo(self, event): | ||
1514 | + print "Redo" | ||
1515 | + Publisher.sendMessage('Redo edition') | ||
1516 | + | ||
1517 | + def OnToggle(self, event): | ||
1518 | + """ | ||
1519 | + Update status of toolbar item (bitmap and help) | ||
1520 | + """ | ||
1521 | + id = event.GetId() | ||
1522 | + if id == ID_LAYOUT: | ||
1523 | + self.ToggleLayout() | ||
1524 | + elif id== ID_TEXT: | ||
1525 | + self.ToggleText() | ||
1526 | + | ||
1527 | + for item in VIEW_TOOLS: | ||
1528 | + state = self.GetToolState(item) | ||
1529 | + if state and (item != id): | ||
1530 | + self.ToggleTool(item, False) | ||
1531 | + | ||
1532 | + def SetStateProjectClose(self): | ||
1533 | + """ | ||
1534 | + Disable menu items (e.g. text) when project is closed. | ||
1535 | + """ | ||
1536 | + self.ontool_text = True | ||
1537 | + self.ToggleText() | ||
1538 | + for tool in self.enable_items: | ||
1539 | + self.EnableTool(tool, False) | ||
1540 | + | ||
1541 | + def SetStateProjectOpen(self): | ||
1542 | + """ | ||
1543 | + Disable menu items (e.g. text) when project is closed. | ||
1544 | + """ | ||
1545 | + self.ontool_text = False | ||
1546 | + self.ToggleText() | ||
1547 | + for tool in self.enable_items: | ||
1548 | + self.EnableTool(tool, True) | ||
1549 | + | ||
1550 | + def ToggleLayout(self): | ||
1551 | + """ | ||
1552 | + Based on previous layout item state, toggle it. | ||
1553 | + """ | ||
1554 | + if self.ontool_layout: | ||
1555 | + self.SetToolNormalBitmap(ID_LAYOUT,self.BMP_WITHOUT_MENU) | ||
1556 | + Publisher.sendMessage('Show task panel') | ||
1557 | + self.SetToolShortHelp(ID_LAYOUT,_("Hide task panel")) | ||
1558 | + self.ontool_layout = False | ||
1559 | + else: | ||
1560 | + self.bitmap = self.BMP_WITH_MENU | ||
1561 | + self.SetToolNormalBitmap(ID_LAYOUT,self.BMP_WITH_MENU) | ||
1562 | + Publisher.sendMessage('Hide task panel') | ||
1563 | + self.SetToolShortHelp(ID_LAYOUT, _("Show task panel")) | ||
1564 | + self.ontool_layout = True | ||
1565 | + | ||
1566 | + def ToggleText(self): | ||
1567 | + """ | ||
1568 | + Based on previous text item state, toggle it. | ||
1569 | + """ | ||
1570 | + if self.ontool_text: | ||
1571 | + self.SetToolNormalBitmap(ID_TEXT,self.BMP_WITH_TEXT) | ||
1572 | + Publisher.sendMessage('Hide text actors on viewers') | ||
1573 | + self.SetToolShortHelp(ID_TEXT,_("Show text")) | ||
1574 | + Publisher.sendMessage('Update AUI') | ||
1575 | + self.ontool_text = False | ||
1576 | + else: | ||
1577 | + self.SetToolNormalBitmap(ID_TEXT, self.BMP_WITHOUT_TEXT) | ||
1578 | + Publisher.sendMessage('Show text actors on viewers') | ||
1579 | + self.SetToolShortHelp(ID_TEXT,_("Hide text")) | ||
1580 | + Publisher.sendMessage('Update AUI') | ||
1581 | + self.ontool_text = True | ||
1582 | + | ||
1583 | + def OnEnableUndo(self, pubsub_evt): | ||
1584 | + value = pubsub_evt.data | ||
1585 | + if value: | ||
1586 | + self.EnableTool(wx.ID_UNDO, True) | ||
1587 | + else: | ||
1588 | + self.EnableTool(wx.ID_UNDO, False) | ||
1589 | + | ||
1590 | + def OnEnableRedo(self, pubsub_evt): | ||
1591 | + value = pubsub_evt.data | ||
1592 | + if value: | ||
1593 | + self.EnableTool(wx.ID_REDO, True) | ||
1594 | + else: | ||
1595 | + self.EnableTool(wx.ID_REDO, False) |