CMS 3D CMS Logo

TabController.py
Go to the documentation of this file.
1 import os.path
2 import logging
3 import math
4 
5 from PyQt4.QtCore import QObject
6 from PyQt4.QtGui import QMessageBox,QInputDialog
7 
8 from Vispa.Main.Filetype import Filetype
9 from Vispa.Share.UndoEvent import UndoEvent
10 
11 class TabController(QObject):
12  """ Base class for all tab controllers.
13 
14  Tab controllers control the functionality of plugin tabs.
15  """
16 
17  TAB_LABEL_MAX_LENGTH = 20
18 
19  def __init__(self, plugin):
20  QObject.__init__(self)
21  logging.debug(__name__ + ": __init__")
22  self._plugin = plugin
23  self._fileModifiedFlag = False
24  self._isEditableFlag = True
25  self._tab = None
26  self._filename = None
27  self._copyPasteEnabledFlag = False
28  self._allowSelectAllFlag = False
29  self._findEnabledFlag = False
30  self._userZoomLevel = 100
34  self._supportsUndo = False
35  self._undoEvents = []
36  self._redoEvents = []
37 
38  #@staticmethod
40  """ Static function returning all filetypes the tab controller can handle.
41 
42  Sub classes should reimplement this function. It returns a list with 2-tuples of the following form:
43  ('extension', 'description of file type').
44  """
45  return []
46  staticSupportedFileTypes = staticmethod(staticSupportedFileTypes)
47 
48  def supportedFileTypes(self):
49  """ Returns staticSupportedFileTypes() of the class to which this object belongs.
50  """
51  return self.__class__.staticSupportedFileTypes()
52 
54  supportedFileTypes = self.__class__.staticSupportedFileTypes()
55  return ";;".join([Filetype(t[0], t[1]).fileDialogFilter() for t in supportedFileTypes])
56 
57  def plugin(self):
58  """ Returns the plugin reference, set by setPlugin().
59  """
60  return self._plugin
61 
62  def setTab(self, tab):
63  """ Sets tab.
64  """
65  self._tab = tab
66 
67  def tab(self):
68  """ Returns tab.
69  """
70  return self._tab
71 
72  def setFilename(self, filename):
73  """ Sets a filename.
74  """
75  self._filename = filename
76  if self._filename and os.path.exists(self._filename):
77  self._fileModifcationTimestamp = os.path.getmtime(self._filename)
78 
79  def filename(self):
80  """ Returns filename of this tab.
81  """
82  return self._filename
83 
84  def getFileBasename(self):
85  """ Returns the basename of this tab's filename.
86 
87  Part of filename after last /.
88  """
89  return os.path.basename(self._filename)
90 
91  def setCopyPasteEnabled(self, enable=True):
92  """ Sets a flag indicating whether this tab can handle copy and paste events.
93 
94  See also isCopyPasteEnabled(), cut(), copy(), paste().
95  """
96  self._copyPasteEnabledFlag = enable
97 
98  def isCopyPasteEnabled(self):
99  """ Return True if the copyPasteFlag is set.
100 
101  See setCopyPasteEnabled(), cut(), copy(), paste().
102  """
103  return self._copyPasteEnabledFlag
104 
105  def setFindEnabled(self, enable=True):
106  """Sets a flag indicating whether this tab can handle find requests.
107 
108  See isFindEnabled(), find().
109  """
110  self._findEnabledFlag = enable
111 
112  def isFindEnabled(self):
113  """Returns True if findEnabledFlag is set.
114 
115  See setFindEnabled(), find().
116  """
117  return self._findEnabledFlag
118 
119  def updateLabel(self,prefix="",titletext = ""):
120  """ Sets the text of the tab to filename if it is set. If
121  titletext
122  is not an emty string, it is used instead of the filename.
123 
124  Otherwise it is set to 'UNTITLED'. It also evaluates the fileModifiedFlag and indicates changes with an *.
125  """
126  if self._filename:
127  title = os.path.basename(self._filename)
128  if len(os.path.splitext(title)[0]) > self.TAB_LABEL_MAX_LENGTH:
129  ext = os.path.splitext(title)[1].lower().strip(".")
130  title = os.path.splitext(title)[0][0:self.TAB_LABEL_MAX_LENGTH] + "...." + ext
131  elif titletext == "":
132  title = 'UNTITLED'
133  else:
134  title = titletext
135 
136  if self.isModified():
137  title = '*' + title
138 
139  title = prefix+title
140 
141  if self.tab().tabWidget():
142  self.tab().tabWidget().setTabText(self.tab().tabWidget().indexOf(self.tab()), title)
143  else:
144  self.tab().setWindowTitle(title)
145 
146  def setModified(self, modified=True):
147  """ Sets the file Modified flag to True or False.
148 
149  This affects the closing of this tab.
150  It is only possible to set the modification flag to True if the controller is editable (see isEditable()).
151  """
152  if modified and not self.isEditable():
153  return
154 
155  previous = self._fileModifiedFlag
156  self._fileModifiedFlag = modified
157 
158  if previous != self._fileModifiedFlag:
159  self.updateLabel()
160  if self.tab().mainWindow():
161  self.plugin().application().updateMenuAndWindowTitle()
162  else:
163  logging.info(self.__class__.__name__ +": setModified() - Cannot tell application the modification state: There is no application associated with the tab.")
164 
165  def isModified(self):
166  """ Evaluates the file Modified flag. Always returns True if no filename is set.
167  """
168  return self._fileModifiedFlag
169 
170  def setEditable(self, editable):
171  """ Sets the file Editable flag.
172  """
173  #logging.debug(self.__class__.__name__ +": setEditable(" + str(editable) +")")
174  self._isEditableFlag = editable
175  self.plugin().application().updateMenu()
176 
177  def isEditable(self):
178  """ Evaluates the file Editable flag.
179  """
180  return self._isEditableFlag
181 
182  def setAllowSelectAll(self, allowSelectAll):
183  """ Sets the allowSelectAll flag.
184  """
185  self._allowSelectAllFlag = allowSelectAll
186  self.plugin().application().updateMenu()
187 
188  def allowSelectAll(self):
189  """ Evaluates the sllowSelectAll flag.
190  """
191  return self._allowSelectAllFlag
192 
193  def open(self, filename=None, update=True):
194  """ Open given file.
195  """
196  logging.debug(self.__class__.__name__ + ": open()")
197 
198  statusMessage = self.plugin().application().startWorking("Opening file " + filename)
199 
200  if filename == None:
201  if self._filename:
202  filename = self._filename
203  else:
204  self.plugin().application().stopWorking(statusMessage, "failed")
205  return False
206 
207  if self.readFile(filename):
208  self.setFilename(filename)
209  self.updateLabel()
210  if update:
211  self.updateContent()
212  self.plugin().application().stopWorking(statusMessage)
213  return True
214 
215  self.plugin().application().stopWorking(statusMessage, "failed")
216  return False
217 
218  def readFile(self, filename):
219  """
220  This function performs the actual reading of a file. It should be overwritten by any PluginTab which inherits Tab.
221  If the reading was successful True should be returned.
222  The file should be read from the file given in the argument filename not to the one in self._filename.
223  """
224  raise NotImplementedError
225 
226  def save(self, filename=""):
227  """ Takes care the tab's data will be written to the file given as argument or if this is an empty string to self._filename.
228 
229  Whenever the content of the tab should be saved, this method should be called. If no filename is specified nor already set set it asks the user to set one.
230  Afterwards the writing is initiated by calling writeFile().
231  """
232  #logging.debug('Tab: save()')
233 
234  if filename == '':
235  if self._filename:
236  filename = self._filename
237  else:
238  return self.plugin().application().saveFileAsDialog()
239 
240  statusMessage = self.plugin().application().startWorking("Saving file " + filename)
241 
242  good=True
243  message=""
244  try:
245  good=self.writeFile(filename)
246  except Exception as e:
247  good=False
248  message="\n"+str(e)
249  if good:
250  self.setFilename(filename)
251  self.setModified(False)
252  self.updateLabel()
253  self.plugin().application().addRecentFile(filename)
254  self.plugin().application().updateMenuAndWindowTitle()
255 
256  # set last saved state for undo events
257  if len(self._redoEvents) > 0:
258  lastSavedStateEvent = self._redoEvents[len(self._redoEvents) -1]
259  else:
260  lastSavedStateEvent = None
261  self.setLastSavedStateEvent(lastSavedStateEvent)
262 
263  self.plugin().application().stopWorking(statusMessage)
264  return True
265  else:
266  QMessageBox.critical(self.tab().mainWindow(), 'Error while saving data', 'Could not write to file ' + filename +'.'+message)
267  logging.error(self.__class__.__name__ + ": save() : Could not write to file " + filename +'.'+message)
268  self.plugin().application().stopWorking(statusMessage, "failed")
269  return False
270 
271  def allowClose(self):
272  if self.isEditable() and self.isModified():
273  messageResult = self.plugin().application().showMessageBox("The document has been modified.",
274  "Do you want to save your changes?",
275  QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
276  QMessageBox.Save)
277 
278  if messageResult == QMessageBox.Save:
279  if not self.save():
280  return False
281  elif messageResult == QMessageBox.Cancel:
282  return False
283  return True
284 
285  def close(self):
286  """ Asks user if he wants to save potentially unsaved data and closes the tab.
287 
288  This function usually does not need to be overwritten by a PluginTab.
289  """
290  allowClose = self.allowClose()
291  if allowClose:
292  if self.tab().tabWidget():
293  self.tab().tabWidget().removeTab(self.tab().tabWidget().indexOf(self.tab()))
294  #if self.tab() in self.tab().mainWindow()._tabWindows:
295  # self.tab().mainWindow()._tabWindows.remove(self.tab())
296  self.tab().close()
297  # no effect?
298  #self._tab.deleteLater()
299  #self._tab = None
300  return allowClose
301 
302  def writeFile(self, filename):
303  """
304  This function performs the actual writing / saving of a file. It should be overwritten by any PluginTab which inherits Tab.
305  If the writing was successful True should be returned.
306  The file should be written to the file given in the argument filename not to the one in self._filename.
307  These variables may differ in case the user selects "save as..." and picks a new filename on a file which already has a name set.
308  If writing was successful the self._filename variable will then be set to the value of filename.
309  """
310  raise NotImplementedError
311 
313  """ Compares the actual modification timestamp of self.filename() to the modification at opening or last save operation.
314 
315  This function is called by Application when the tab associated with this controller was activated.
316  If modification timestamps differ the refresh() method is called.
317  """
318  if not self._filename or self._fileModifcationTimestamp == 0:
319  return
320 
321  msgBox = None
322  if not os.path.exists(self._filename):
323  logging.debug(self.__class__.__name__ + ": checkModificationTimestamp() - File was removed.")
325  msgBox = QMessageBox()
326  msgBox.setText("The file was removed.")
327  if self.isEditable():
328  msgBox.setInformativeText("Do you want to save the file with your version?")
329  saveButton = msgBox.addButton("Save", QMessageBox.ActionRole)
330  ignoreButton = msgBox.addButton("Ignore", QMessageBox.RejectRole)
331  else:
332  ignoreButton = msgBox.addButton("OK", QMessageBox.RejectRole)
333  reloadButton = None
334 
335  elif self._fileModifcationTimestamp != os.path.getmtime(self._filename):
336  logging.debug(self.__class__.__name__ + ": checkModificationTimestamp() - File was modified.")
337  msgBox = QMessageBox()
338  msgBox.setText("The file has been modified.")
339  if self.isEditable():
340  msgBox.setInformativeText("Do you want to overwrite the file with your version or reload the file?")
341  saveButton = msgBox.addButton("Overwrite", QMessageBox.ActionRole)
342  else:
343  msgBox.setInformativeText("Do you want to reload the file?")
344  reloadButton = msgBox.addButton("Reload", QMessageBox.DestructiveRole)
345  ignoreButton = msgBox.addButton("Ignore", QMessageBox.RejectRole)
346 
347  if msgBox and not self._showingModifiedMessageFlag:
348  self.setModified()
350  msgBox.exec_()
351  self._showingModifiedMessageFlag=False
352 
353  if self.isEditable() and msgBox.clickedButton() == saveButton:
354  self.save()
355  elif msgBox.clickedButton() == reloadButton:
356  self.refresh()
357  self.setModified(False)
358  elif msgBox.clickedButton() == ignoreButton and os.path.exists(self._filename):
359  self._fileModifcationTimestamp = os.path.getmtime(self._filename)
360 
361  else:
362  #logging.debug(self.__class__.__name__ + ": checkModificationTimestamp() - File was not modified.")
363  pass
364 
365  def activated(self):
366  """ Called by application when tab is activated in tabWidget.
367 
368  This function should be overwritten if special treatment on tab selection is required.
369  """
370  pass
371 
372  def cut(self):
373  """ Handle cut event.
374 
375  This function is called if the user selects 'Cut' from menu. PluginTabs should override it if needed.
376  See also setCopyPasteEnabled(), isCopyPasteEnabled().
377  """
378  raise NotImplementedError
379 
380  def copy(self):
381  """ Handle copy event.
382 
383  This function is called if the user selects 'Copy' from menu. PluginTabs should override it if needed.
384  See also setCopyPasteEnabled(), isCopyPasteEnabled().
385  """
386  raise NotImplementedError
387 
388  def paste(self):
389  """ Handle paste event.
390 
391  This function is called if the user selects 'Paste' from menu. PluginTabs should override it if needed."
392  See also setCopyPasteEnabled(), isCopyPasteEnabled().
393  """
394  raise NotImplementedError
395 
396  def find(self):
397  """ Handle find event.
398 
399  This function is called if the user selects 'Find' from menu. PluginTabs should override it if needed."
400  See also setFindEnabled(), isFindEnabled().
401  """
402  raise NotImplementedError
403 
404  def selectAll(self):
405  """ Handle to perform select all action.
406 
407  This function is called if the user selects 'Select all' from menu. PluginTabs should override it if needed."
408  See also setAllowSelectAll(), allowSelectAll().
409  """
410  raise NotImplementedError
411 
412  def setZoom(self, zoom):
413  """ This function has to be implemented by tab controllers who want to use the zoom toolbar.
414 
415  The implementation has to forward the zoom value to the Zoomable object for which the toolbar is set up.
416  See also zoom()
417  """
418  raise NotImplementedError
419 
420  def zoom(self):
421  """ This function has to be implemented by tab controllers who want to use the zoom toolbar.
422 
423  The implementation should return the zoom value of the Zoomable object for which the toolbar is set up.
424  See also setZoom()
425  """
426  raise NotImplementedError
427 
428  def zoomChanged(self, zoom):
429  """ Shows zoom value on main window's status bar.
430  """
431  self.tab().mainWindow().statusBar().showMessage("Zoom " + str(round(zoom)) + " %")
432 
434  """ Sets the zoom button pressed before flag to False.
435 
436  If the flag is set functions handling the zoom toolbar buttons (zoomHundred(), zoomAll()) wont store the last zoom factor. The flag is set to true by these functions.
437  By this mechanism the user can click the zoom buttons several times and will still be able to return to his orignal zoom level by zoomUser().
438  The reset function needs to be called if the user manually sets the zoom level. For instance by connecting this function to the wheelEvent of the workspace scroll area.
439  """
440  self._zoomButtonPressedBeforeFlag = False
441 
442  def zoomUser(self):
443  """ Returns to the manually set zoom factor before zoomHundred() or zoomAll() were called.
444  """
445  logging.debug(__name__ + ": zoomUser()")
446  self.setZoom(self._userZoomLevel)
447 
448  def zoomHundred(self):
449  """ Sets zoom factor to 100 %.
450  """
451  logging.debug(__name__ + ": zoomHundred()")
452  if not self._zoomButtonPressedBeforeFlag:
453  self._userZoomLevel = self.zoom()
454  self._zoomButtonPressedBeforeFlag = True
455  self.setZoom(100)
456 
457  def zoomAll(self):
458  """ Zooms workspace content to fit optimal.
459 
460  Currently only works if scroll area is used and accessible through self.tab().scrollArea().
461  """
462  logging.debug(__name__ + ": zoomAll()")
463  if not self._zoomButtonPressedBeforeFlag:
464  self._userZoomLevel = self.zoom()
465  self._zoomButtonPressedBeforeFlag = True
466 
467  viewportWidth = self.tab().scrollArea().viewport().width()
468  viewportHeight = self.tab().scrollArea().viewport().height()
469 
470  for i in range(0, 2):
471  # do 2 iterations to prevent rounding error --> better fit
472  workspaceChildrenRect = self.tab().scrollArea().widget().childrenRect()
473  widthRatio = self.zoom() * viewportWidth / (workspaceChildrenRect.right())
474  heightRatio = self.zoom() * viewportHeight / (workspaceChildrenRect.bottom())
475 
476  if widthRatio > heightRatio:
477  ratio = heightRatio
478  else:
479  ratio = widthRatio
480 
481  self.setZoom(math.floor(ratio))
482 
483  def updateContent(self):
484  """ Called after file is loaded.
485 
486  Meant to update to Tab content.
487  """
488  raise NotImplementedError
489 
490  def refresh(self):
491  """ Reloads file content and refreshes tab.
492 
493  May be implemented by inheriting controllers.
494  """
495  statusMessage = self.plugin().application().startWorking("Reopening file")
496  self._fileModifcationTimestamp = os.path.getmtime(self._filename)
497  self.readFile(self._filename)
498  self.updateContent()
499  self.plugin().application().stopWorking(statusMessage)
500 
501  def zoomDialog(self):
502  if hasattr(QInputDialog, "getInteger"):
503  # Qt 4.3
504  (zoom, ok) = QInputDialog.getInteger(self.tab(), "Zoom...", "Input zoom factor in percent:", self.zoom(), 0)
505  else:
506  # Qt 4.5
507  (zoom, ok) = QInputDialog.getInt(self.tab(), "Zoom...", "Input zoom factor in percent:", self.zoom(), 0)
508  if ok:
509  self.setZoom(zoom)
510  self._userZoomLevel = zoom
511 
512  def cancel(self):
513  """ Cancel all operations in tab.
514 
515  This function is called when all current operations in tab shall be canceled.
516  """
517  pass
518 
519  def supportsUndo(self):
520  """ Returns True if the this tab controller supports undo history.
521  """
522  return self._supportsUndo
523 
524  def enableUndo(self, enable=True):
525  """ If enable is True this controller enables its undo function.
526 
527  For any tab controller that wants to use this feature, it needs to be made sure the corresponding UndoEvents
528  for actions that should be undoable exists and are added by addUndoEvent().
529  """
530  self._supportsUndo = enable
531 
532  def undoEvents(self):
533  """ Returns list of all registered UndoEvents.
534  """
535  return self._undoEvents
536 
537  def redoEvents(self):
538  """ Returns list of all UndoEvents that have already been undone before.
539  """
540  return self._redoEvents
541 
542  def addUndoEvent(self, undoEvent):
543  """ Adds event of type UndoEvent to this tab controller's list of undoable events.
544 
545  Undo can be invoked by calling undo().
546  """
547  if self._supportsUndo:
548  if not isinstance(undoEvent, UndoEvent):
549  logging.error("%s: Tried to add non-UndoEvent to list of undo events. Aborting..." % self.__class__.__name__)
550  return
551  self._redoEvents = []
552  no_of_events = len(self._undoEvents)
553  # try to combine similar events
554  if no_of_events < 1 or not self._undoEvents[no_of_events -1].combine(undoEvent):
555  self._undoEvents.append(undoEvent)
556  if not self.isModified():
557  self.setLastSavedStateEvent(undoEvent)
558  self.plugin().application().updateMenu()
559  else:
560  logging.warning("%s: Tried to add undo event, however undo functionality is not enabled. Aborting..." % self.__class__.__name__)
561  #self.dumpUndoEvents()
562 
563  def undo(self, numberOfEvents=1):
564  """ Invokes undo of last stored UndoEvent (see addUndoEvent()), if undo functionality is enabled.
565  """
566  if not self._supportsUndo:
567  logging.warning(self.__class__.__name__ + ": Tried to undo action, however undo functionality is not enabled. Aborting...")
568  return
569  logging.debug(self.__class__.__name__ +": undo("+ str(numberOfEvents) +")")
570  #self.dumpUndoEvents()
571 
572  for i in range(0, numberOfEvents):
573  if len(self._undoEvents) > 0:
574  lastEvent = self._undoEvents.pop()
575  lastEvent.undo()
576  self._redoEvents.append(lastEvent)
577 
578  # set modification flag
579  if i == (numberOfEvents -1):
580  if lastEvent.isLastSavedState():
581  self.setModified(False)
582  else:
583  self.setModified()
584  self.plugin().application().updateMenu()
585 
586  #self.dumpUndoEvents()
587 
588  def redo(self, numberOfEvents=1):
589  if not self._supportsUndo:
590  logging.warning(self.__class__.__name__ + ": Tried to undo action, however undo functionality is not enabled. Aborting...")
591  return
592  logging.debug(self.__class__.__name__ +": redo("+ str(numberOfEvents) +")")
593  #self.dumpUndoEvents()
594 
595  for i in range(0, numberOfEvents):
596  if len(self._redoEvents) > 0:
597  lastEvent = self._redoEvents.pop()
598  lastEvent.redo()
599  self._undoEvents.append(lastEvent)
600 
601  # set modification flag
602  if i == (numberOfEvents -1):
603  undo_events_count = len(self._redoEvents)
604  if undo_events_count > 0:
605  if self._redoEvents[undo_events_count-1].isLastSavedState():
606  self.setModified(False)
607  else:
608  self.setModified()
609  else:
610  # if there are no more redo events
611  # and there is no event with last saved state flag set to True
612  # no action was performed since the last save
613  # and thus modification flag should be False
614  modified = False
615  for event in self._undoEvents + self._redoEvents:
616  if event.isLastSavedState():
617  modified = True
618  break
619  self.setModified(modified)
620  self.plugin().application().updateMenu()
621 
622  def setLastSavedStateEvent(self, undoEvent):
623  """ Sets last saved state flag of given UndoEvent to True and to False for all other events.
624  """
625  for current_event in self._undoEvents + self._redoEvents:
626  if current_event == undoEvent:
627  current_event.setLastSavedState(True)
628  else:
629  current_event.setLastSavedState(False)
630 
631  def dumpUndoEvents(self):
632  for event in self._undoEvents:
633  event.dump("undo")
634  for event in self._redoEvents:
635  event.dump("redo")
def setFindEnabled(self, enable=True)
def setModified(self, modified=True)
def open(self, filename=None, update=True)
def undo(self, numberOfEvents=1)
def setCopyPasteEnabled(self, enable=True)
static std::string join(char **cmd)
Definition: RemoteFile.cc:18
def redo(self, numberOfEvents=1)
def setLastSavedStateEvent(self, undoEvent)
def updateLabel(self, prefix="", titletext="")
def setAllowSelectAll(self, allowSelectAll)