--- a/Snapshot/SnapWidget.py Sun Apr 08 12:45:44 2012 +0200 +++ b/Snapshot/SnapWidget.py Mon Apr 09 17:02:31 2012 +0200 @@ -7,14 +7,24 @@ Module implementing the snapshot widget. """ -from PyQt4.QtCore import pyqtSlot, QFile, QFileInfo -from PyQt4.QtGui import QWidget, QImageWriter, QApplication +# +# SnapWidget and it's associated modules are PyQt4 ports of Ksnapshot. +# + +import os + +from PyQt4.QtCore import pyqtSlot, QFile, QFileInfo, QTimer, QPoint, QMimeData, Qt, \ + QEvent, QRegExp +from PyQt4.QtGui import QWidget, QImageWriter, QApplication, QPixmap, QCursor, QDrag, \ + QShortcut, QKeySequence, QDesktopServices from E5Gui import E5FileDialog, E5MessageBox from .Ui_SnapWidget import Ui_SnapWidget from .SnapshotRegionGrabber import SnapshotRegionGrabber +from .SnapshotFreehandGrabber import SnapshotFreehandGrabber +from .SnapshotTimer import SnapshotTimer import UI.PixmapCache import Preferences @@ -24,6 +34,12 @@ """ Class implementing the snapshot widget. """ + ModeFullscreen = 0 + ModeScreen = 1 + ModeRectangle = 2 + ModeFreehand = 3 + ModeEllipse = 4 + def __init__(self, parent=None): """ Constructor @@ -39,19 +55,53 @@ self.setWindowIcon(UI.PixmapCache.getIcon("ericSnap.png")) self.modeCombo.addItem(self.trUtf8("Fullscreen"), - SnapshotRegionGrabber.ModeFullscreen) + SnapWidget.ModeFullscreen) self.modeCombo.addItem(self.trUtf8("Rectangular Selection"), - SnapshotRegionGrabber.ModeRectangle) - mode = int(Preferences.Prefs.settings.value("Snapshot/Mode", 0)) - index = self.modeCombo.findData(mode) + SnapWidget.ModeRectangle) + self.modeCombo.addItem(self.trUtf8("Ellipical Selection"), + SnapWidget.ModeEllipse) + self.modeCombo.addItem(self.trUtf8("Freehand Selection"), + SnapWidget.ModeFreehand) + if QApplication.desktop().numScreens() > 1: + self.modeCombo.addItem(self.trUtf8("Current Screen"), + SnapWidget.ModeScreen) + self.__mode = int(Preferences.Prefs.settings.value("Snapshot/Mode", 0)) + index = self.modeCombo.findData(self.__mode) + if index == -1: + index = 0 self.modeCombo.setCurrentIndex(index) - self.delaySpin.setValue(int(Preferences.Prefs.settings.value( - "Snapshot/Delay", 0))) + self.__delay = int(Preferences.Prefs.settings.value( "Snapshot/Delay", 0)) + self.delaySpin.setValue(self.__delay) + + self.__filename = Preferences.Prefs.settings.value( "Snapshot/Filename", + os.path.join( + QDesktopServices.storageLocation(QDesktopServices.PicturesLocation), + self.trUtf8("snapshot") + "1.png")) self.__grabber = None + self.__snapshot = QPixmap() + self.__savedPosition = QPoint() + self.__modified = False + + self.__grabberWidget = QWidget(None, Qt.X11BypassWindowManagerHint) + self.__grabberWidget.move(-10000, -10000) + self.__grabberWidget.installEventFilter(self); self.__initFileFilters() + + self.__initShortcuts() + + self.preview.startDrag.connect(self.__dragSnapshot) + + self.__grabTimer = SnapshotTimer() + self.__grabTimer.timeout.connect(self.__grabTimerTimeout) + self.__updateTimer = QTimer() + self.__updateTimer.setSingleShot(True) + self.__updateTimer.timeout.connect(self.__updatePreview) + + self.__updateCaption() + self.takeButton.setFocus() def __initFileFilters(self): """ @@ -88,26 +138,81 @@ self.__defaultFilter = filters['png'] - @pyqtSlot(bool) - def on_saveButton_clicked(self, checked): + def __initShortcuts(self): + """ + Private method to initialize the keyboard shortcuts. + """ + self.__quitShortcut = QShortcut(QKeySequence(QKeySequence.Quit), self, self.close) + + self.__copyShortcut = QShortcut(QKeySequence(QKeySequence.Copy), self, + self.copyButton.animateClick) + + self.__quickSaveShortcut = QShortcut(QKeySequence(Qt.Key_Q), self, + self.__quickSave) + + self.__save1Shortcut = QShortcut(QKeySequence(QKeySequence.Save), self, + self.saveButton.animateClick) + self.__save2Shortcut = QShortcut(QKeySequence(Qt.Key_S), self, + self.saveButton.animateClick) + + self.__grab1Shortcut = QShortcut(QKeySequence(QKeySequence.New), self, + self.takeButton.animateClick) + self.__grab2Shortcut = QShortcut(QKeySequence(Qt.Key_N), self, + self.takeButton.animateClick) + self.__grab3Shortcut = QShortcut(QKeySequence(Qt.Key_Space), self, + self.takeButton.animateClick) + + def __quickSave(self): + """ + Private slot to save the snapshot bypassing the file selection dialog. + """ + if not self.__snapshot.isNull(): + while os.path.exists(self.__filename): + self.__autoIncFilename() + + if self.__saveImage(self.__filename): + self.__modified = False + self.__autoIncFilename() + self.__updateCaption() + + @pyqtSlot() + def on_saveButton_clicked(self): """ Private slot to save the snapshot. """ - fileName, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( - self, - self.trUtf8("Save Snapshot"), - "", - self.__outputFilter, - self.__defaultFilter, - E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) - if not fileName: - return + if not self.__snapshot.isNull(): + while os.path.exists(self.__filename): + self.__autoIncFilename() + + fileName, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( + self, + self.trUtf8("Save Snapshot"), + self.__filename, + self.__outputFilter, + self.__defaultFilter, + E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) + if not fileName: + return + + ext = QFileInfo(fileName).suffix() + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fileName += ex + + if self.__saveImage(fileName): + self.__modified = False + self.__filename = fileName + self.__autoIncFilename() + self.__updateCaption() + + def __saveImage(self, fileName): + """ + Private method to save the snapshot. - ext = QFileInfo(fileName).suffix() - if not ext: - ex = selectedFilter.split("(*")[1].split(")")[0] - if ex: - fileName += ex + @param fileName name of the file to save to (string) + @return flag indicating success (boolean) + """ if QFileInfo(fileName).exists(): res = E5MessageBox.yesNo(self, self.trUtf8("Save Snapshot"), @@ -115,39 +220,160 @@ " Overwrite it?</p>").format(fileName), icon=E5MessageBox.Warning) if not res: - return + return False file = QFile(fileName) if not file.open(QFile.WriteOnly): E5MessageBox.warning(self, self.trUtf8("Save Snapshot"), self.trUtf8("Cannot write file '{0}:\n{1}.")\ .format(fileName, file.errorString())) - return + return False - res = self.preview.pixmap().save(file) + ok = self.__snapshot.save(file) file.close() - if not res: + if not ok: E5MessageBox.warning(self, self.trUtf8("Save Snapshot"), self.trUtf8("Cannot write file '{0}:\n{1}.")\ .format(fileName, file.errorString())) + + return ok - @pyqtSlot(bool) - def on_takeButton_clicked(self, checked): + def __autoIncFilename(self): + """ + Private method to auto-increment the file name. + """ + # Extract the file name + name = os.path.basename(self.__filename) + + # If the name contains a number, then increment it. + numSearch = QRegExp("(^|[^\\d])(\\d+)") # We want to match as far left as + # possible, and when the number is + # at the start of the name. + + # Does it have a number? + start = numSearch.lastIndexIn(name) + if start != -1: + # It has a number, increment it. + start = numSearch.pos(2) # Only the second group is of interest. + numAsStr = numSearch.capturedTexts()[2] + number = "{0:0{width}d}".format(int(numAsStr) + 1, width=len(numAsStr)) + name = name[:start] + number + name[start+len(numAsStr):] + else: + # no number + start = name.rfind('.') + if start != -1: + # has a '.' somewhere, e.g. it has an extension + name = name [:start] + '1' + name[start:] + else: + # no extension, just tack it on to the end + name += '1' + + self.__filename = os.path.join(os.path.dirname(self.__filename), name) + self.__updateCaption() + + @pyqtSlot() + def on_takeButton_clicked(self): """ Private slot to take a snapshot. """ - mode = self.modeCombo.itemData(self.modeCombo.currentIndex()) - delay = self.delaySpin.value() + self.__mode = self.modeCombo.itemData(self.modeCombo.currentIndex()) + self.__delay = self.delaySpin.value() - Preferences.Prefs.settings.setValue("Snapshot/Delay", delay) - Preferences.Prefs.settings.setValue("Snapshot/Mode", mode) - + self.__savedPosition = self.pos() self.hide() - self.__grabber = SnapshotRegionGrabber(mode, delay) + if self.__delay: + self.__grabTimer.start(self.__delay) + else: + QTimer.singleShot(200, self.__startUndelayedGrab) + + def __grabTimerTimeout(self): + """ + Private slot to perform a delayed grab operation. + """ + if self.__mode == SnapWidget.ModeRectangle: + self.__grabRectangle() + elif self.__mode == SnapWidget.ModeEllipse: + self.__grabEllipse() + elif self.__mode == SnapWidget.ModeFreehand: + self.__grabFreehand() + else: + self.__performGrab() + + def __startUndelayedGrab(self): + """ + Private slot to perform an undelayed grab operation. + """ + if self.__mode == SnapWidget.ModeRectangle: + self.__grabRectangle() + elif self.__mode == SnapWidget.ModeEllipse: + self.__grabEllipse() + elif self.__mode == SnapWidget.ModeFreehand: + self.__grabFreehand() + else: + self.__grabberWidget.show() + self.__grabberWidget.grabMouse(Qt.CrossCursor) + + def __grabRectangle(self): + """ + Private method to grab a rectangular screen region. + """ + self.__grabber = SnapshotRegionGrabber(mode=SnapshotRegionGrabber.Rectangle) + self.__grabber.grabbed.connect(self.__captured) + + def __grabEllipse(self): + """ + Private method to grab an elliptical screen region. + """ + self.__grabber = SnapshotRegionGrabber(mode=SnapshotRegionGrabber.Ellipse) self.__grabber.grabbed.connect(self.__captured) + def __grabFreehand(self): + """ + Private method to grab a non-rectangular screen region. + """ + self.__grabber = SnapshotFreehandGrabber() + self.__grabber.grabbed.connect(self.__captured) + + def __performGrab(self): + """ + Private method to perform a screen grab other than a selected region. + """ + self.__grabberWidget.releaseMouse() + self.__grabberWidget.hide() + self.__grabTimer.stop() + + if self.__mode == SnapWidget.ModeFullscreen: + self.__snapshot = QPixmap.grabWindow(QApplication.desktop().winId()) + elif self.__mode == SnapWidget.ModeScreen: + desktop = QApplication.desktop() + screenId = desktop.screenNumber(QCursor.pos()) + geom = desktop.screenGeometry(screenId) + x = geom.x() + y = geom.y() + self.__snapshot = QPixmap.grabWindow( + desktop.winId(), x, y, geom.width(), geom.height()) + else: + self.__snapshot = QPixmap() + + self.__redisplay() + self.__modified = True + self.__updateCaption() + + def __redisplay(self): + """ + Private method to redisplay the window. + """ + self.__updatePreview() + QApplication.restoreOverrideCursor() + if not self.__savedPosition.isNull(): + self.move(self.__savedPosition) + self.show() + + self.saveButton.setEnabled(not self.__snapshot.isNull()) + self.copyButton.setEnabled(not self.__snapshot.isNull()) + @pyqtSlot() def on_copyButton_clicked(self): """ @@ -162,10 +388,92 @@ @param pixmap pixmap of the snapshot (QPixmap) """ self.__grabber.close() - self.preview.setPixmap(pixmap) + self.__snapshot = QPixmap(pixmap) + self.__grabber.grabbed.disconnect(self.__captured) self.__grabber = None - self.saveButton.setEnabled(True) - self.copyButton.setEnabled(True) - self.show() + self.__redisplay() + self.__modified = True + self.__updateCaption() + + def __updatePreview(self): + """ + Private slot to update the preview picture. + """ + self.preview.setToolTip( + self.trUtf8("Preview of the snapshot image ({0:n} x {1:n})").format( + self.__snapshot.width(), self.__snapshot.height())) + self.preview.setPreview(self.__snapshot) + self.preview.adjustSize() + + def resizeEvent(self, evt): + """ + Protected method handling a resizing of the window. + + @param evt resize event (QResizeEvent) + """ + self.__updateTimer.start(200) + + def __dragSnapshot(self): + """ + Private slot handling the dragging of the preview picture. + """ + drag = QDrag(self) + mimeData = QMimeData() + mimeData.setImageData(self.__snapshot) + drag.setMimeData(mimeData) + drag.setPixmap(self.preview.pixmap()) + drag.exec_(Qt.CopyAction) + + def eventFilter(self, obj, evt): + """ + Public method to handle event for other objects. + + @param obj reference to the object (QObject) + @param evt reference to the event (QEvent) + @return flag indicating that the event should be filtered out (boolean) + """ + if obj == self.__grabberWidget and evt.type() == QEvent.MouseButtonPress: + if QWidget.mouseGrabber() != self.__grabberWidget: + return False + if evt.button() == Qt.LeftButton: + self.__performGrab() + + return False + + def closeEvent(self, evt): + """ + Protected method handling the close event. + + @param evt close event (QCloseEvent) + """ + if self.__modified: + res = E5MessageBox.question(self, + self.trUtf8("eric5 Snapshot"), + self.trUtf8("""The application contains an unsaved snapshot."""), + E5MessageBox.StandardButtons( + E5MessageBox.Abort | \ + E5MessageBox.Discard | \ + E5MessageBox.Save)) + if res == E5MessageBox.Abort: + evt.ignore() + return + elif res == E5MessageBox.Save: + self.on_saveButton_clicked() + + Preferences.Prefs.settings.setValue("Snapshot/Delay", self.delaySpin.value()) + Preferences.Prefs.settings.setValue("Snapshot/Mode", + self.modeCombo.itemData(self.modeCombo.currentIndex())) + Preferences.Prefs.settings.setValue("Snapshot/Filename", self.__filename) + Preferences.Prefs.settings.sync() + + def __updateCaption(self): + """ + Private method to update the window caption. + """ + self.setWindowTitle("{0}[*] - {1}".format( + os.path.basename(self.__filename), + self.trUtf8("eric5 Snapshot"))) + self.setWindowModified(self.__modified) + self.pathNameEdit.setText(os.path.dirname(self.__filename))