CMS 3D CMS Logo

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