class VidCutter(QWidget): def __init__(self, parent): super(VidCutter, self).__init__(parent) self.parent = parent self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.VideoSurface) self.videoWidget = VideoWidget() self.videoService = VideoService(self) QFontDatabase.addApplicationFont( os.path.join(self.getAppPath(), 'fonts', 'DroidSansMono.ttf')) QFontDatabase.addApplicationFont( os.path.join(self.getAppPath(), 'fonts', 'HelveticaNeue.ttf')) qApp.setFont(QFont('Helvetica Neue', 10)) self.clipTimes = [] self.inCut = False self.movieFilename = '' self.movieLoaded = False self.timeformat = 'hh:mm:ss' self.finalFilename = '' self.totalRuntime = 0 self.initIcons() self.initActions() self.toolbar = QToolBar( floatable=False, movable=False, iconSize=QSize(28, 28), toolButtonStyle=Qt.ToolButtonTextUnderIcon, styleSheet= 'QToolBar QToolButton { min-width:82px; margin-left:10px; margin-right:10px; font-size:14px; }' ) self.initToolbar() self.aboutMenu, self.cliplistMenu = QMenu(), QMenu() self.initMenus() self.seekSlider = VideoSlider(parent=self, sliderMoved=self.setPosition) self.seekSlider.installEventFilter(self) self.initNoVideo() self.cliplist = QListWidget( sizePolicy=QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding), contextMenuPolicy=Qt.CustomContextMenu, uniformItemSizes=True, iconSize=QSize(100, 700), dragDropMode=QAbstractItemView.InternalMove, alternatingRowColors=True, customContextMenuRequested=self.itemMenu, styleSheet='QListView::item { margin:10px 5px; }') self.cliplist.setFixedWidth(185) self.cliplist.model().rowsMoved.connect(self.syncClipList) listHeader = QLabel(pixmap=QPixmap( os.path.join(self.getAppPath(), 'images', 'clipindex.png'), 'PNG'), alignment=Qt.AlignCenter) listHeader.setStyleSheet( '''padding:5px; padding-top:8px; border:1px solid #b9b9b9; border-bottom:none; background-color:qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #FFF, stop: 0.5 #EAEAEA, stop: 0.6 #EAEAEA stop:1 #FFF);''' ) self.runtimeLabel = QLabel('<div align="right">00:00:00</div>', textFormat=Qt.RichText) self.runtimeLabel.setStyleSheet( '''font-family:Droid Sans Mono; font-size:10pt; color:#FFF; background:rgb(106, 69, 114) url(:images/runtime.png) no-repeat left center; padding:2px; padding-right:8px; border:1px solid #b9b9b9; border-top:none;''' ) self.clipindexLayout = QVBoxLayout(spacing=0) self.clipindexLayout.setContentsMargins(0, 0, 0, 0) self.clipindexLayout.addWidget(listHeader) self.clipindexLayout.addWidget(self.cliplist) self.clipindexLayout.addWidget(self.runtimeLabel) self.videoLayout = QHBoxLayout() self.videoLayout.setContentsMargins(0, 0, 0, 0) self.videoLayout.addWidget(self.novideoWidget) self.videoLayout.addLayout(self.clipindexLayout) self.timeCounter = QLabel('00:00:00 / 00:00:00', autoFillBackground=True, alignment=Qt.AlignCenter, sizePolicy=QSizePolicy( QSizePolicy.Expanding, QSizePolicy.Fixed)) self.timeCounter.setStyleSheet( 'color:#FFF; background:#000; font-family:Droid Sans Mono; font-size:10.5pt; padding:4px;' ) videoplayerLayout = QVBoxLayout(spacing=0) videoplayerLayout.setContentsMargins(0, 0, 0, 0) videoplayerLayout.addWidget(self.videoWidget) videoplayerLayout.addWidget(self.timeCounter) self.videoplayerWidget = QWidget(self, visible=False) self.videoplayerWidget.setLayout(videoplayerLayout) self.menuButton = QPushButton(icon=self.aboutIcon, flat=True, toolTip='About', statusTip='About', iconSize=QSize(24, 24), cursor=Qt.PointingHandCursor) self.menuButton.setMenu(self.aboutMenu) self.muteButton = QPushButton(icon=self.unmuteIcon, flat=True, toolTip='Mute', statusTip='Toggle audio mute', cursor=Qt.PointingHandCursor, clicked=self.muteAudio) self.volumeSlider = QSlider(Qt.Horizontal, toolTip='Volume', statusTip='Adjust volume level', cursor=Qt.PointingHandCursor, value=50, sizePolicy=QSizePolicy( QSizePolicy.Fixed, QSizePolicy.Minimum), minimum=0, maximum=100, sliderMoved=self.setVolume) self.volumeSlider.setStyleSheet( '''QSlider::groove:horizontal { height:40px; } QSlider::sub-page:horizontal { border:1px outset #6A4572; background:#6A4572; margin:2px; } QSlider::handle:horizontal { image: url(:images/knob.png) no-repeat top left; width:20px; }''' ) self.saveAction = QPushButton( self.parent, icon=self.saveIcon, text='Save Video', flat=True, toolTip='Save Video', clicked=self.cutVideo, cursor=Qt.PointingHandCursor, iconSize=QSize(30, 30), statusTip='Save video clips merged as a new video file', enabled=False) self.saveAction.setStyleSheet( '''QPushButton { color:#FFF; padding:8px; font-size:12pt; border:1px inset #481953; border-radius:4px; background-color:rgb(106, 69, 114); } QPushButton:!enabled { background-color:rgba(0, 0, 0, 0.1); color:rgba(0, 0, 0, 0.3); border:1px inset #CDCDCD; } QPushButton:hover { background-color:rgba(255, 255, 255, 0.8); color:#444; } QPushButton:pressed { background-color:rgba(218, 218, 219, 0.8); color:#444; }''' ) controlsLayout = QHBoxLayout() controlsLayout.addStretch(1) controlsLayout.addWidget(self.toolbar) controlsLayout.addSpacerItem(QSpacerItem(20, 1)) controlsLayout.addWidget(self.saveAction) controlsLayout.addStretch(1) controlsLayout.addWidget(self.muteButton) controlsLayout.addWidget(self.volumeSlider) controlsLayout.addSpacing(1) controlsLayout.addWidget(self.menuButton) layout = QVBoxLayout() layout.setContentsMargins(10, 10, 10, 4) layout.addLayout(self.videoLayout) layout.addWidget(self.seekSlider) layout.addLayout(controlsLayout) self.setLayout(layout) self.mediaPlayer.setVideoOutput(self.videoWidget) self.mediaPlayer.stateChanged.connect(self.mediaStateChanged) self.mediaPlayer.positionChanged.connect(self.positionChanged) self.mediaPlayer.durationChanged.connect(self.durationChanged) self.mediaPlayer.error.connect(self.handleError) def initNoVideo(self) -> None: novideoImage = QLabel(alignment=Qt.AlignCenter, autoFillBackground=False, pixmap=QPixmap( os.path.join(self.getAppPath(), 'images', 'novideo.png'), 'PNG'), sizePolicy=QSizePolicy( QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)) novideoImage.setBackgroundRole(QPalette.Dark) novideoImage.setContentsMargins(0, 20, 0, 20) self.novideoLabel = QLabel(alignment=Qt.AlignCenter, autoFillBackground=True, sizePolicy=QSizePolicy( QSizePolicy.Expanding, QSizePolicy.Minimum)) self.novideoLabel.setBackgroundRole(QPalette.Dark) self.novideoLabel.setContentsMargins(0, 20, 15, 60) novideoLayout = QVBoxLayout(spacing=0) novideoLayout.addWidget(novideoImage) novideoLayout.addWidget(self.novideoLabel, alignment=Qt.AlignTop) self.novideoMovie = QMovie( os.path.join(self.getAppPath(), 'images', 'novideotext.gif')) self.novideoMovie.frameChanged.connect(self.setNoVideoText) self.novideoMovie.start() self.novideoWidget = QWidget(self, autoFillBackground=True) self.novideoWidget.setBackgroundRole(QPalette.Dark) self.novideoWidget.setLayout(novideoLayout) def initIcons(self) -> None: self.appIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'vidcutter.png')) self.openIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'addmedia.png')) self.playIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'play.png')) self.pauseIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'pause.png')) self.cutStartIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'cut-start.png')) self.cutEndIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'cut-end.png')) self.saveIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'save.png')) self.muteIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'muted.png')) self.unmuteIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'unmuted.png')) self.upIcon = QIcon(os.path.join(self.getAppPath(), 'images', 'up.png')) self.downIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'down.png')) self.removeIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'remove.png')) self.removeAllIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'remove-all.png')) self.successIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'success.png')) self.aboutIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'about.png')) self.completePlayIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'complete-play.png')) self.completeOpenIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'complete-open.png')) self.completeRestartIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'complete-restart.png')) self.completeExitIcon = QIcon( os.path.join(self.getAppPath(), 'images', 'complete-exit.png')) def initActions(self) -> None: self.openAction = QAction(self.openIcon, 'Add Media', self, statusTip='Select media source', triggered=self.openFile) self.playAction = QAction(self.playIcon, 'Play Video', self, statusTip='Play selected media', triggered=self.playVideo, enabled=False) self.cutStartAction = QAction(self.cutStartIcon, 'Set Start', self, toolTip='Set Start', statusTip='Set start marker', triggered=self.cutStart, enabled=False) self.cutEndAction = QAction(self.cutEndIcon, 'Set End', self, statusTip='Set end marker', triggered=self.cutEnd, enabled=False) self.moveItemUpAction = QAction( self.upIcon, 'Move Up', self, statusTip='Move clip position up in list', triggered=self.moveItemUp, enabled=False) self.moveItemDownAction = QAction( self.downIcon, 'Move Down', self, statusTip='Move clip position down in list', triggered=self.moveItemDown, enabled=False) self.removeItemAction = QAction( self.removeIcon, 'Remove clip', self, statusTip='Remove selected clip from list', triggered=self.removeItem, enabled=False) self.removeAllAction = QAction(self.removeAllIcon, 'Clear list', self, statusTip='Clear all clips from list', triggered=self.clearList, enabled=False) self.aboutAction = QAction('About %s' % qApp.applicationName(), self, statusTip='Credits and acknowledgements', triggered=self.aboutInfo) self.aboutQtAction = QAction('About Qt', self, statusTip='About Qt', triggered=qApp.aboutQt) self.mediaInfoAction = QAction( 'Media Information', self, statusTip='Media information from loaded video file', triggered=self.mediaInfo, enabled=False) def initToolbar(self) -> None: self.toolbar.addAction(self.openAction) self.toolbar.addAction(self.playAction) self.toolbar.addSeparator() self.toolbar.addAction(self.cutStartAction) self.toolbar.addAction(self.cutEndAction) self.toolbar.addSeparator() def initMenus(self) -> None: self.aboutMenu.addAction(self.mediaInfoAction) self.aboutMenu.addSeparator() self.aboutMenu.addAction(self.aboutQtAction) self.aboutMenu.addAction(self.aboutAction) self.cliplistMenu.addAction(self.moveItemUpAction) self.cliplistMenu.addAction(self.moveItemDownAction) self.cliplistMenu.addSeparator() self.cliplistMenu.addAction(self.removeItemAction) self.cliplistMenu.addAction(self.removeAllAction) def setRunningTime(self, runtime: str) -> None: self.runtimeLabel.setText('<div align="right">%s</div>' % runtime) @pyqtSlot(int) def setNoVideoText(self, frame: int) -> None: self.novideoLabel.setPixmap(self.novideoMovie.currentPixmap()) def itemMenu(self, pos: QPoint) -> None: globalPos = self.cliplist.mapToGlobal(pos) self.moveItemUpAction.setEnabled(False) self.moveItemDownAction.setEnabled(False) self.removeItemAction.setEnabled(False) self.removeAllAction.setEnabled(False) index = self.cliplist.currentRow() if index != -1: if not self.inCut: if index > 0: self.moveItemUpAction.setEnabled(True) if index < self.cliplist.count() - 1: self.moveItemDownAction.setEnabled(True) if self.cliplist.count() > 0: self.removeItemAction.setEnabled(True) if self.cliplist.count() > 0: self.removeAllAction.setEnabled(True) self.cliplistMenu.exec_(globalPos) def moveItemUp(self) -> None: index = self.cliplist.currentRow() tmpItem = self.clipTimes[index] del self.clipTimes[index] self.clipTimes.insert(index - 1, tmpItem) self.renderTimes() def moveItemDown(self) -> None: index = self.cliplist.currentRow() tmpItem = self.clipTimes[index] del self.clipTimes[index] self.clipTimes.insert(index + 1, tmpItem) self.renderTimes() def removeItem(self) -> None: index = self.cliplist.currentRow() del self.clipTimes[index] if self.inCut and index == self.cliplist.count() - 1: self.inCut = False self.initMediaControls() self.renderTimes() def clearList(self) -> None: self.clipTimes.clear() self.cliplist.clear() self.inCut = False self.renderTimes() self.initMediaControls() def mediaInfo(self) -> None: if self.mediaPlayer.isMetaDataAvailable(): content = '<table cellpadding="4">' for key in self.mediaPlayer.availableMetaData(): val = self.mediaPlayer.metaData(key) if type(val) is QSize: val = '%s x %s' % (val.width(), val.height()) content += '<tr><td align="right"><b>%s:</b></td><td>%s</td></tr>\n' % ( key, val) content += '</table>' mbox = QMessageBox(windowTitle='Media Information', windowIcon=self.parent.windowIcon(), textFormat=Qt.RichText) mbox.setText('<b>%s</b>' % os.path.basename( self.mediaPlayer.currentMedia().canonicalUrl().toLocalFile())) mbox.setInformativeText(content) mbox.exec_() else: QMessageBox.critical( self.parent, 'Could not retrieve media information', '''There was a problem in tring to retrieve media information. This DOES NOT mean there is a problem with the file and you should be able to continue using it.''') def aboutInfo(self) -> None: about_html = '''<style> a { color:#441d4e; text-decoration:none; font-weight:bold; } a:hover { text-decoration:underline; } </style> <p style="font-size:26pt; font-weight:bold;">%s</p> <p> <span style="font-size:13pt;"><b>Version: %s</b></span> <span style="font-size:10pt;position:relative;left:5px;">( %s )</span> </p> <p style="font-size:13px;"> Copyright © 2016 <a href="mailto:[email protected]">Pete Alexandrou</a> <br/> Website: <a href="%s">%s</a> </p> <p style="font-size:13px;"> Thanks to the folks behind the <b>Qt</b>, <b>PyQt</b> and <b>FFmpeg</b> projects for all their hard and much appreciated work. </p> <p style="font-size:11px;"> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. </p> <p style="font-size:11px;"> This software uses libraries from the <a href="https://www.ffmpeg.org">FFmpeg</a> project under the <a href="https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html">LGPLv2.1</a> </p>''' % (qApp.applicationName(), qApp.applicationVersion(), platform.architecture()[0], qApp.organizationDomain(), qApp.organizationDomain()) QMessageBox.about(self.parent, 'About %s' % qApp.applicationName(), about_html) def openFile(self) -> None: filename, _ = QFileDialog.getOpenFileName(self.parent, caption='Select video', directory=QDir.homePath()) if filename != '': self.loadFile(filename) def loadFile(self, filename: str) -> None: self.movieFilename = filename if not os.path.exists(filename): return self.mediaPlayer.setMedia(QMediaContent(QUrl.fromLocalFile(filename))) self.initMediaControls(True) self.cliplist.clear() self.clipTimes = [] self.parent.setWindowTitle( '%s - %s' % (qApp.applicationName(), os.path.basename(filename))) if not self.movieLoaded: self.videoLayout.replaceWidget(self.novideoWidget, self.videoplayerWidget) self.novideoMovie.stop() self.novideoMovie.deleteLater() self.novideoWidget.deleteLater() self.videoplayerWidget.show() self.videoWidget.show() self.movieLoaded = True if self.mediaPlayer.isVideoAvailable(): self.mediaPlayer.setPosition(1) self.mediaPlayer.play() self.mediaPlayer.pause() def playVideo(self) -> None: if self.mediaPlayer.state() == QMediaPlayer.PlayingState: self.mediaPlayer.pause() self.playAction.setText('Play Video') else: self.mediaPlayer.play() self.playAction.setText('Pause Video') def initMediaControls(self, flag: bool = True) -> None: self.playAction.setEnabled(flag) self.saveAction.setEnabled(False) self.cutStartAction.setEnabled(flag) self.cutEndAction.setEnabled(False) self.mediaInfoAction.setEnabled(flag) if flag: self.seekSlider.setRestrictValue(0) def setPosition(self, position: int) -> None: self.mediaPlayer.setPosition(position) def positionChanged(self, progress: int) -> None: self.seekSlider.setValue(progress) currentTime = self.deltaToQTime(progress) totalTime = self.deltaToQTime(self.mediaPlayer.duration()) self.timeCounter.setText('%s / %s' % (currentTime.toString( self.timeformat), totalTime.toString(self.timeformat))) @pyqtSlot() def mediaStateChanged(self) -> None: if self.mediaPlayer.state() == QMediaPlayer.PlayingState: self.playAction.setIcon(self.pauseIcon) else: self.playAction.setIcon(self.playIcon) def durationChanged(self, duration: int) -> None: self.seekSlider.setRange(0, duration) def muteAudio(self, muted: bool) -> None: if self.mediaPlayer.isMuted(): self.mediaPlayer.setMuted(not self.mediaPlayer.isMuted()) self.muteButton.setIcon(self.unmuteIcon) self.muteButton.setToolTip('Mute') else: self.mediaPlayer.setMuted(not self.mediaPlayer.isMuted()) self.muteButton.setIcon(self.muteIcon) self.muteButton.setToolTip('Unmute') def setVolume(self, volume: int) -> None: self.mediaPlayer.setVolume(volume) def toggleFullscreen(self) -> None: self.videoWidget.setFullScreen(not self.videoWidget.isFullScreen()) def cutStart(self) -> None: self.clipTimes.append([ self.deltaToQTime(self.mediaPlayer.position()), '', self.captureImage() ]) self.cutStartAction.setDisabled(True) self.cutEndAction.setEnabled(True) self.seekSlider.setRestrictValue(self.seekSlider.value() + 1000) self.mediaPlayer.setPosition(self.seekSlider.restrictValue) self.inCut = True self.renderTimes() def cutEnd(self) -> None: item = self.clipTimes[len(self.clipTimes) - 1] selected = self.deltaToQTime(self.mediaPlayer.position()) if selected.__lt__(item[0]): QMessageBox.critical( self.parent, 'Invalid END Time', 'The clip end time must come AFTER it\'s start time. Please try again.' ) return item[1] = selected self.cutStartAction.setEnabled(True) self.cutEndAction.setDisabled(True) self.seekSlider.setRestrictValue(0) self.inCut = False self.renderTimes() @pyqtSlot(QModelIndex, int, int, QModelIndex, int) def syncClipList(self, parent: QModelIndex, start: int, end: int, destination: QModelIndex, row: int) -> None: if start < row: index = row - 1 else: index = row clip = self.clipTimes.pop(start) self.clipTimes.insert(index, clip) def renderTimes(self) -> None: self.cliplist.clear() self.seekSlider.setCutMode(self.inCut) if len(self.clipTimes) > 4: self.cliplist.setFixedWidth(200) else: self.cliplist.setFixedWidth(185) self.totalRuntime = 0 for item in self.clipTimes: endItem = '' if type(item[1]) is QTime: endItem = item[1].toString(self.timeformat) self.totalRuntime += item[0].msecsTo(item[1]) listitem = QListWidgetItem() listitem.setTextAlignment(Qt.AlignVCenter) if type(item[2]) is QPixmap: listitem.setIcon(QIcon(item[2])) self.cliplist.addItem(listitem) marker = QLabel( '''<style>b { font-size:8pt; } p { margin:5px; }</style> <p><b>START</b><br/>%s</p><p><b>END</b><br/>%s</p>''' % (item[0].toString(self.timeformat), endItem)) self.cliplist.setItemWidget(listitem, marker) listitem.setFlags(Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsEnabled) if len(self.clipTimes) and not self.inCut: self.saveAction.setEnabled(True) if self.inCut or len(self.clipTimes) == 0 or not type( self.clipTimes[0][1]) is QTime: self.saveAction.setEnabled(False) self.setRunningTime( self.deltaToQTime(self.totalRuntime).toString(self.timeformat)) @staticmethod def deltaToQTime(millisecs: int) -> QTime: secs = millisecs / 1000 return QTime((secs / 3600) % 60, (secs / 60) % 60, secs % 60, (secs * 1000) % 1000) def captureImage(self) -> None: frametime = self.deltaToQTime( self.mediaPlayer.position()).addSecs(1).toString(self.timeformat) inputfile = self.mediaPlayer.currentMedia().canonicalUrl().toLocalFile( ) imagecap = self.videoService.capture(inputfile, frametime) if type(imagecap) is QPixmap: return imagecap def cutVideo(self) -> bool: self.setCursor(Qt.BusyCursor) clips = len(self.clipTimes) filename, filelist = '', [] source = self.mediaPlayer.currentMedia().canonicalUrl().toLocalFile() _, sourceext = os.path.splitext(source) if clips > 0: self.finalFilename, _ = QFileDialog.getSaveFileName( self.parent, 'Save video', source, 'Video files (*%s)' % sourceext) if self.finalFilename != '': self.saveAction.setDisabled(True) self.showProgress(clips) file, ext = os.path.splitext(self.finalFilename) index = 1 self.progress.setLabelText('Cutting video clips...') qApp.processEvents() for clip in self.clipTimes: duration = self.deltaToQTime(clip[0].msecsTo( clip[1])).toString(self.timeformat) filename = '%s_%s%s' % (file, '{0:0>2}'.format(index), ext) filelist.append(filename) self.videoService.cut(source, filename, clip[0].toString(self.timeformat), duration) index += 1 if len(filelist) > 1: self.joinVideos(filelist, self.finalFilename) else: QFile.remove(self.finalFilename) QFile.rename(filename, self.finalFilename) self.unsetCursor() self.progress.setLabelText('Complete...') qApp.processEvents() self.saveAction.setEnabled(True) self.progress.close() self.progress.deleteLater() self.complete() self.saveAction.setEnabled(True) self.unsetCursor() self.saveAction.setDisabled(True) return True self.unsetCursor() self.saveAction.setDisabled(True) return False def joinVideos(self, joinlist: list, filename: str) -> None: listfile = os.path.normpath( os.path.join(os.path.dirname(joinlist[0]), '.vidcutter.list')) fobj = open(listfile, 'w') for file in joinlist: fobj.write('file \'%s\'\n' % file.replace("'", "\\'")) fobj.close() self.videoService.join(listfile, filename) try: QFile.remove(listfile) for file in joinlist: if os.path.isfile(file): QFile.remove(file) except: pass def showProgress(self, steps: int, label: str = 'Processing video...') -> None: self.progress = QProgressDialog(label, None, 0, steps, self.parent, windowModality=Qt.ApplicationModal, windowIcon=self.parent.windowIcon(), minimumDuration=0, minimumWidth=500) self.progress.show() for i in range(steps): self.progress.setValue(i) qApp.processEvents() time.sleep(1) def complete(self) -> None: info = QFileInfo(self.finalFilename) mbox = QMessageBox(windowTitle='Success', windowIcon=self.parent.windowIcon(), minimumWidth=500, iconPixmap=self.successIcon.pixmap(48, 49), textFormat=Qt.RichText) mbox.setText( ''' <style> table.info { margin:8px; padding:4px 15px; } td.label { font-weight:bold; font-size:9pt; text-align:right; background-color:#444; color:#FFF; } td.value { background-color:#FFF !important; font-size:10pt; } </style> <p>Your video was successfully created.</p> <p align="center"> <table class="info" cellpadding="6" cellspacing="0"> <tr> <td class="label"><b>Filename</b></td> <td class="value" nowrap>%s</td> </tr> <tr> <td class="label"><b>Size</b></td> <td class="value">%s</td> </tr> <tr> <td class="label"><b>Runtime</b></td> <td class="value">%s</td> </tr> </table> </p> <p>How would you like to proceed?</p>''' % (QDir.toNativeSeparators( self.finalFilename), self.sizeof_fmt(int(info.size())), self.deltaToQTime(self.totalRuntime).toString(self.timeformat))) play = mbox.addButton('Play', QMessageBox.AcceptRole) play.setIcon(self.completePlayIcon) play.clicked.connect(self.openResult) fileman = mbox.addButton('Open', QMessageBox.AcceptRole) fileman.setIcon(self.completeOpenIcon) fileman.clicked.connect(self.openFolder) end = mbox.addButton('Exit', QMessageBox.AcceptRole) end.setIcon(self.completeExitIcon) end.clicked.connect(self.close) new = mbox.addButton('Restart', QMessageBox.AcceptRole) new.setIcon(self.completeRestartIcon) new.clicked.connect(self.startNew) mbox.setDefaultButton(new) mbox.setEscapeButton(new) mbox.exec_() def sizeof_fmt(self, num: float, suffix: chr = 'B') -> str: for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: if abs(num) < 1024.0: return "%3.1f%s%s" % (num, unit, suffix) num /= 1024.0 return "%.1f%s%s" % (num, 'Y', suffix) @pyqtSlot() def openFolder(self) -> None: self.openResult(pathonly=True) @pyqtSlot(bool) def openResult(self, pathonly: bool = False) -> None: self.startNew() if len(self.finalFilename) and os.path.exists(self.finalFilename): target = self.finalFilename if not pathonly else os.path.dirname( self.finalFilename) QDesktopServices.openUrl(QUrl.fromLocalFile(target)) @pyqtSlot() def startNew(self) -> None: self.unsetCursor() self.clearList() self.seekSlider.setValue(0) self.seekSlider.setRange(0, 0) self.mediaPlayer.setMedia(QMediaContent()) self.initNoVideo() self.videoLayout.replaceWidget(self.videoplayerWidget, self.novideoWidget) self.initMediaControls(False) self.parent.setWindowTitle('%s' % qApp.applicationName()) def wheelEvent(self, event: QWheelEvent) -> None: if self.mediaPlayer.isVideoAvailable( ) or self.mediaPlayer.isAudioAvailable(): if event.angleDelta().y() > 0: newval = self.seekSlider.value() - 1000 else: newval = self.seekSlider.value() + 1000 self.seekSlider.setValue(newval) self.seekSlider.setSliderPosition(newval) self.mediaPlayer.setPosition(newval) event.accept() def keyPressEvent(self, event: QKeyEvent) -> None: if self.mediaPlayer.isVideoAvailable( ) or self.mediaPlayer.isAudioAvailable(): addtime = 0 if event.key() == Qt.Key_Left: addtime = -1000 elif event.key() == Qt.Key_PageUp or event.key() == Qt.Key_Up: addtime = -10000 elif event.key() == Qt.Key_Right: addtime = 1000 elif event.key() == Qt.Key_PageDown or event.key() == Qt.Key_Down: addtime = 10000 elif event.key() == Qt.Key_Enter: self.toggleFullscreen() elif event.key( ) == Qt.Key_Escape and self.videoWidget.isFullScreen(): self.videoWidget.setFullScreen(False) if addtime != 0: newval = self.seekSlider.value() + addtime self.seekSlider.setValue(newval) self.seekSlider.setSliderPosition(newval) self.mediaPlayer.setPosition(newval) event.accept() def mousePressEvent(self, event: QMouseEvent) -> None: if event.button() == Qt.BackButton and self.cutStartAction.isEnabled(): self.cutStart() event.accept() elif event.button( ) == Qt.ForwardButton and self.cutEndAction.isEnabled(): self.cutEnd() event.accept() else: super(VidCutter, self).mousePressEvent(event) def eventFilter(self, obj: QObject, event: QEvent) -> bool: if event.type() == QEvent.MouseButtonRelease and isinstance( obj, VideoSlider): if obj.objectName() == 'VideoSlider' and ( self.mediaPlayer.isVideoAvailable() or self.mediaPlayer.isAudioAvailable()): obj.setValue( QStyle.sliderValueFromPosition(obj.minimum(), obj.maximum(), event.x(), obj.width())) self.mediaPlayer.setPosition(obj.sliderPosition()) return QWidget.eventFilter(self, obj, event) @pyqtSlot(QMediaPlayer.Error) def handleError(self, error: QMediaPlayer.Error) -> None: self.unsetCursor() self.startNew() if error == QMediaPlayer.ResourceError: QMessageBox.critical( self.parent, 'Error', 'Invalid media file detected at:<br/><br/><b>%s</b><br/><br/>%s' % (self.movieFilename, self.mediaPlayer.errorString())) else: QMessageBox.critical(self.parent, 'Error', self.mediaPlayer.errorString()) def getAppPath(self) -> str: return ':' def closeEvent(self, event: QCloseEvent) -> None: self.parent.closeEvent(event)
class GuiPrala(QMainWindow): HEIGHT = 220 WIDTH = 550 BASIC_FONT = "Arial" BASIC_SIZE = 10 BASIC_COLOR = Qt.black BASIC_BG = Qt.white BASIC_ITALIC = False BASIC_BOLD = False def __init__(self, file_name="", part_of_speech_filter="", extra_filter="", setup=None): super().__init__() # self.file_name = file_name # self.part_of_speech_filter = part_of_speech_filter # self.extra_filter = extra_filter # # --- Tool Bar --- # # OPEN open_action = QAction( QIcon( resource_filename(__name__, "/".join(("images", "open-tool.png")))), _("MAIN.TOOLBAR.OPEN"), self) open_action.setShortcut("Ctrl+O") open_action.triggered.connect(self.open_dict_file) # START self.start_action = QAction( QIcon( resource_filename(__name__, "/".join(("images", "start-tool.png")))), _("MAIN.TOOLBAR.START"), self) self.start_action.setShortcut("Ctrl+S") self.start_action.triggered.connect(self.startClicked) # SAY OUT self.sayout_action = QAction( QIcon( resource_filename(__name__, "/".join(("images", "say-tool.png")))), _("MAIN.TOOLBAR.SAYOUT"), self) self.sayout_action.setShortcut("Ctrl+T") self.sayout_action.triggered.connect(self.sayOut) spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # QUIT quit_action = QAction( QIcon( resource_filename(__name__, "/".join(("images", "quit-tool.png")))), _("MAIN.TOOLBAR.QUIT"), self) quit_action.setShortcut("Ctrl+Q") quit_action.triggered.connect(QApplication.instance().quit) # ENABLE TO SAY enable_to_say_icon = QIcon() enable_to_say_icon.addPixmap( QPixmap( resource_filename( __name__, "/".join( ("images", "enable-to-say-on-tool.png")))), QIcon.Normal, QIcon.On) enable_to_say_icon.addPixmap( QPixmap( resource_filename( __name__, "/".join( ("images", "enable-to-say-off-tool.png")))), QIcon.Normal, QIcon.Off) self.enable_to_say_button = QToolButton() self.enable_to_say_button.setFocusPolicy(Qt.NoFocus) self.enable_to_say_button.setIcon(enable_to_say_icon) self.enable_to_say_button.setCheckable(True) self.enable_to_say_button.toggled.connect(self.changeEnableToSay) # ENABLE TO SHOW NOTE enable_to_show_note_icon = QIcon() enable_to_show_note_icon.addPixmap( QPixmap( resource_filename( __name__, "/".join( ("images", "enable-to-show-note-on-tool.png")))), QIcon.Normal, QIcon.On) enable_to_show_note_icon.addPixmap( QPixmap( resource_filename( __name__, "/".join( ("images", "enable-to-show-note-off-tool.png")))), QIcon.Normal, QIcon.Off) self.enable_to_show_note_button = QToolButton() self.enable_to_show_note_button.setFocusPolicy(Qt.NoFocus) self.enable_to_show_note_button.setIcon(enable_to_show_note_icon) self.enable_to_show_note_button.setCheckable(True) self.enable_to_show_note_button.toggled.connect( self.changeEnableToShowNote) # ENABLE TO SHOW PATTERN enable_to_show_pattern_icon = QIcon() enable_to_show_pattern_icon.addPixmap( QPixmap( resource_filename( __name__, "/".join( ("images", "enable-to-show-pattern-on-tool.png")))), QIcon.Normal, QIcon.On) enable_to_show_pattern_icon.addPixmap( QPixmap( resource_filename( __name__, "/".join( ("images", "enable-to-show-pattern-off-tool.png")))), QIcon.Normal, QIcon.Off) self.enable_to_show_pattern_button = QToolButton() self.enable_to_show_pattern_button.setFocusPolicy(Qt.NoFocus) self.enable_to_show_pattern_button.setIcon(enable_to_show_pattern_icon) self.enable_to_show_pattern_button.setCheckable(True) self.enable_to_show_pattern_button.toggled.connect( self.changeEnableToShowPattern) # BASE LANGUAGE DROPDOWN self.base_language_dropdown = QComboBox(self) self.base_language_dropdown.setFocusPolicy(Qt.NoFocus) engine = pyttsx3.init() voices = engine.getProperty('voices') #flag = QIcon( resource_filename(__name__, "/".join(("images", "open-tool.png"))) ) #[base_language_dropdown.addItem( flag, removeControlChars( i.languages[0].decode("utf-8") ) ) for i in voices] # Eliminates the languages having longer name as 2 [ self.base_language_dropdown.addItem(j) for j in [ removeControlChars(i.languages[0].decode("utf-8")) for i in voices ] if len(j) == 2 ] #[self.base_language_dropdown.addItem( removeControlChars( i.languages[0].decode("utf-8") ) ) for i in voices] self.base_language_dropdown.activated[str].connect( self.changeBaseLanguage) # LEARNING LANGUAGE DROPDOWN self.learning_language_dropdown = QComboBox(self) self.learning_language_dropdown.setFocusPolicy(Qt.NoFocus) #[self.learning_language_dropdown.addItem( removeControlChars( i.languages[0].decode("utf-8") ) ) for i in voices] # Eliminates the languages having longer name as 2 [ self.learning_language_dropdown.addItem(j) for j in [ removeControlChars(i.languages[0].decode("utf-8")) for i in voices ] if len(j) == 2 ] self.learning_language_dropdown.activated[str].connect( self.changeLearningLanguage) # POS FILTER DROPDOWN self.pos_filter_dropdown = QComboBox(self) self.pos_filter_dropdown.setFixedWidth(100) self.pos_filter_dropdown.setFocusPolicy(Qt.NoFocus) self.pos_filter_dropdown.activated[str].connect(self.changePOSFilter) # EXTRA FILTER DROPDOWN self.extra_filter_dropdown = QComboBox(self) self.extra_filter_dropdown.setFixedWidth(100) self.extra_filter_dropdown.setFocusPolicy(Qt.NoFocus) self.extra_filter_dropdown.activated[str].connect( self.changeExtraFilter) # # Default settings # config_ini = ConfigIni.getInstance() self.sayout_action.setEnabled(False) self.start_action.setEnabled(False) self.createAskingCanvas(file_name, part_of_speech_filter, extra_filter, False) self.enable_to_say_button.setChecked(config_ini.isSayOut()) self.enable_to_show_note_button.setChecked(config_ini.isShowNote()) self.enable_to_show_pattern_button.setChecked( config_ini.isShowPattern()) self.base_language_dropdown.setCurrentText( config_ini.getBaseLanguage()) self.learning_language_dropdown.setCurrentText( config_ini.getLearningLanguage()) # Selector Toolbar selectorToolbar = QToolBar("Selector toolbar") self.addToolBar(Qt.LeftToolBarArea, selectorToolbar) selectorToolbar.setMovable(False) selectorToolbar.addWidget(self.pos_filter_dropdown) selectorToolbar.addWidget(self.extra_filter_dropdown) #selectorToolbar.addSeparator() #selectorToolbar.addWidget(self.base_language_dropdown) #selectorToolbar.addWidget(self.learning_language_dropdown) # Main Toolbar mainToolbar = self.addToolBar('Main toolbar') mainToolbar.addAction(open_action) mainToolbar.addSeparator() mainToolbar.addAction(self.start_action) mainToolbar.addAction(self.sayout_action) mainToolbar.addSeparator() mainToolbar.addWidget(self.enable_to_say_button) mainToolbar.addWidget(self.enable_to_show_note_button) mainToolbar.addWidget(self.enable_to_show_pattern_button) mainToolbar.addWidget(self.base_language_dropdown) mainToolbar.addWidget(self.learning_language_dropdown) #mainToolbar.addWidget(self.pos_filter_dropdown) #mainToolbar.addWidget(self.extra_filter_dropdown) mainToolbar.addSeparator() mainToolbar.addWidget(spacer) mainToolbar.addAction(quit_action) # # --- Status Bar --- # #self.statusBar().showMessage("") # # --- Window --- # self.setWindowTitle(setup['title'] + " - " + setup['version']) self.resize(GuiPrala.WIDTH, GuiPrala.HEIGHT) #self.setFixedHeight(GuiPrala.HEIGHT) #self.setFixedWidth(GuiPrala.WIDTH) self.center() self.show() def center(self): """Aligns the window to middle on the screen""" fg = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() fg.moveCenter(cp) self.move(fg.topLeft()) def createAskingCanvas(self, file_name, pos_filter, extra_filter, enabledErrorMessage=True): """ The cases when this method is called: - In the constructor mainT Open a new dict file clicking on the Open toolbar - When the selected element in the filter dropdown changed """ try: # try to open the file and create a selection by the filters self.asking_canvas = AskingCanvas(self.statusBar(), file_name, pos_filter, extra_filter) # if the file and the selectors was OK, then we can change them self.file_name = file_name self.part_of_speech_filter = pos_filter self.extra_filter = extra_filter # Hide CenterWidget self.setCentralWidget(None) # Enable Start button self.start_action.setEnabled(True) # Disable Say out self.sayout_action.setEnabled(False) # Clear the StatusBar self.statusBar().showMessage("") # Fill up the POS Filter selectors self.pos_filter_dropdown.clear() self.pos_filter_dropdown.addItems( self.asking_canvas.myFilteredDictionary.getPOSFilterList()) self.pos_filter_dropdown.setCurrentText(self.part_of_speech_filter) # Fill up the EXTRA Filter selectors self.extra_filter_dropdown.clear() self.extra_filter_dropdown.addItems( self.asking_canvas.myFilteredDictionary.getExtraFilterList()) self.extra_filter_dropdown.setCurrentText(self.extra_filter) except EmptyDictionaryError as e: if enabledErrorMessage: QMessageBox.critical( self, _("ERROR"), _("ERROR_MESSAGE.DICTIONARY_IS_EMPTY") + ":\n" + e.dict_file_name) except NoDictionaryError as f: if enabledErrorMessage: QMessageBox.critical( self, _("ERROR"), _("ERROR_MESSAGE.DICTIONARY_NOT_FOUND") + ":\n" + f.dict_file_name) def open_dict_file(self): """ Opens the file selector dialog window for selecting a dict file. Called when the Open button is clicked """ options = QFileDialog.Options() #options |= QFileDialog.DontUseNativeDialog fileName, sel = QFileDialog.getOpenFileName( self, _("FILE_SELECTOR.TITLE.SELECT_DICT"), "", "Dictionary Files (*" + FilteredDictionary.DICT_EXT + ")", options=options) if fileName: self.createAskingCanvas(fileName, "", "") def startClicked(self): """ -Asking WIdget shown in Central Widget -Start button disabled -Say out button enabled -Answer field in focus -Start the asking round """ self.setCentralWidget(self.asking_canvas) self.start_action.setEnabled(False) self.sayout_action.setEnabled(True) self.asking_canvas.answer_field.setFirstFocus() self.asking_canvas.round() def sayOut(self): if self.asking_canvas.ok_button.status == OKButton.STATUS.ACCEPT: Thread(target=self.asking_canvas.record.say_out_base, args=()).start() else: Thread(target=self.asking_canvas.record.say_out_learning, args=()).start() def changeEnableToSay(self, checked): ConfigIni.getInstance().setSayOut(checked) #self.say_out = checked def changeEnableToShowNote(self, checked): ConfigIni.getInstance().setShowNote(checked) #self.show_note = checked if (not checked and hasattr(self, 'asking_canvas')): self.asking_canvas.note_field.setText("") def changeEnableToShowPattern(self, checked): ConfigIni.getInstance().setShowPattern(checked) if checked and hasattr(self, 'asking_canvas'): self.asking_canvas.answer_field.showPattern() elif hasattr(self, 'asking_canvas'): self.asking_canvas.answer_field.hidePattern() self.asking_canvas.answer_field.setFirstFocus() #self.say_out = checked def changeBaseLanguage(self, text): ConfigIni.getInstance().setBaseLanguage(text) #if there is NO record yet if not self.start_action.isEnabled(): self.asking_canvas.record.setBaseLanguage(text) self.asking_canvas.myFilteredDictionary.setBaseLanguage(text) def changeLearningLanguage(self, text): ConfigIni.getInstance().setLearningLanguage(text) #if there is NO recor yet if not self.start_action.isEnabled(): self.asking_canvas.record.setLearningLanguage(text) self.asking_canvas.myFilteredDictionary.setLearningLanguage(text) def changePOSFilter(self, filter): self.part_of_speech_filter = filter self.createAskingCanvas(self.file_name, filter, self.extra_filter) def changeExtraFilter(self, filter): self.extra_filter = filter self.createAskingCanvas(self.file_name, self.part_of_speech_filter, filter)
class jide(QMainWindow): """This is the primary class which serves as the glue for JIDE. This class interfaces between the various canvases, pixel and color palettes, centralized data source, and data output routines. """ def __init__(self): """jide constructor """ super().__init__() self.setupWindow() self.setupTabs() self.setupDocks() self.setupToolbar() self.setupActions() self.setupStatusBar() self.setupPrefs() def setupWindow(self): """Entry point to set up primary window attributes """ self.setWindowTitle("JIDE") self.sprite_view = QGraphicsView() self.tile_view = QGraphicsView() self.sprite_view.setStyleSheet("background-color: #494949;") self.tile_view.setStyleSheet("background-color: #494949;") def setupDocks(self): """Set up pixel palette, color palette, and tile map docks """ self.sprite_color_palette_dock = ColorPaletteDock(Source.SPRITE, self) self.sprite_pixel_palette_dock = PixelPaletteDock(Source.SPRITE, self) self.tile_color_palette_dock = ColorPaletteDock(Source.TILE, self) self.tile_pixel_palette_dock = PixelPaletteDock(Source.TILE, self) self.addDockWidget(Qt.RightDockWidgetArea, self.sprite_color_palette_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.sprite_pixel_palette_dock) self.removeDockWidget(self.tile_color_palette_dock) self.removeDockWidget(self.tile_pixel_palette_dock) def setupToolbar(self): """Set up graphics tools toolbar """ self.canvas_toolbar = QToolBar() self.addToolBar(Qt.LeftToolBarArea, self.canvas_toolbar) self.tool_actions = QActionGroup(self) self.select_tool = QAction(QIcon(":/icons/select_tool.png"), "&Select tool", self.tool_actions) self.select_tool.setShortcut("S") self.pen_tool = QAction(QIcon(":/icons/pencil_tool.png"), "&Pen tool", self.tool_actions) self.pen_tool.setShortcut("P") self.fill_tool = QAction(QIcon(":/icons/fill_tool.png"), "&Fill tool", self.tool_actions) self.fill_tool.setShortcut("G") self.line_tool = QAction(QIcon(":/icons/line_tool.png"), "&Line tool", self.tool_actions) self.line_tool.setShortcut("L") self.rect_tool = QAction( QIcon(":/icons/rect_tool.png"), "&Rectangle tool", self.tool_actions, ) self.rect_tool.setShortcut("R") self.ellipse_tool = QAction( QIcon(":/icons/ellipse_tool.png"), "&Ellipse tool", self.tool_actions, ) self.ellipse_tool.setShortcut("E") self.tools = [ self.select_tool, self.pen_tool, self.fill_tool, self.line_tool, self.rect_tool, self.ellipse_tool, ] for tool in self.tools: tool.setCheckable(True) tool.setEnabled(False) self.canvas_toolbar.addAction(tool) def setupTabs(self): """Set up main window sprite/tile/tile map tabs """ self.canvas_tabs = QTabWidget() self.canvas_tabs.addTab(self.sprite_view, "Sprites") self.canvas_tabs.addTab(self.tile_view, "Tiles") self.canvas_tabs.setTabEnabled(0, False) self.canvas_tabs.setTabEnabled(1, False) self.setCentralWidget(self.canvas_tabs) def setupActions(self): """Set up main menu actions """ # Exit exit_act = QAction("&Exit", self) exit_act.setShortcut("Ctrl+Q") exit_act.setStatusTip("Exit application") exit_act.triggered.connect(qApp.quit) # Open file open_file = QAction("&Open", self) open_file.setShortcut("Ctrl+O") open_file.setStatusTip("Open file") open_file.triggered.connect(self.selectFile) # Open preferences open_prefs = QAction("&Preferences", self) open_prefs.setStatusTip("Edit preferences") open_prefs.triggered.connect(self.openPrefs) # Undo/redo self.undo_stack = QUndoStack(self) undo_act = self.undo_stack.createUndoAction(self, "&Undo") undo_act.setShortcut(QKeySequence.Undo) redo_act = self.undo_stack.createRedoAction(self, "&Redo") redo_act.setShortcut(QKeySequence.Redo) # Copy/paste self.copy_act = QAction("&Copy", self) self.copy_act.setShortcut("Ctrl+C") self.copy_act.setStatusTip("Copy") self.copy_act.setEnabled(False) self.paste_act = QAction("&Paste", self) self.paste_act.setShortcut("Ctrl+V") self.paste_act.setStatusTip("Paste") self.paste_act.setEnabled(False) # JCAP compile/load self.gendat_act = QAction("&Generate DAT Files", self) self.gendat_act.setShortcut("Ctrl+D") self.gendat_act.setStatusTip("Generate DAT Files") self.gendat_act.triggered.connect(self.genDATFiles) self.gendat_act.setEnabled(False) self.load_jcap = QAction("&Load JCAP System", self) self.load_jcap.setShortcut("Ctrl+L") self.load_jcap.setStatusTip("Load JCAP System") self.load_jcap.setEnabled(False) self.load_jcap.triggered.connect(self.loadJCAP) # Build menu bar menu_bar = self.menuBar() file_menu = menu_bar.addMenu("&File") file_menu.addAction(open_file) file_menu.addSeparator() file_menu.addAction(open_prefs) file_menu.addAction(exit_act) edit_menu = menu_bar.addMenu("&Edit") edit_menu.addAction(undo_act) edit_menu.addAction(redo_act) edit_menu.addAction(self.copy_act) edit_menu.addAction(self.paste_act) jcap_menu = menu_bar.addMenu("&JCAP") jcap_menu.addAction(self.gendat_act) jcap_menu.addAction(self.load_jcap) def setupStatusBar(self): """Set up bottom status bar """ self.statusBar = self.statusBar() def setupPrefs(self): QCoreApplication.setOrganizationName("Connor Spangler") QCoreApplication.setOrganizationDomain("https://github.com/cspang1") QCoreApplication.setApplicationName("JIDE") self.prefs = QSettings() self.prefs.setValue("test", 69) @pyqtSlot(bool) def setCopyActive(self, active): """Set whether the copy action is available :param active: Variable representing whether copy action should be set to available or unavailable :type active: bool """ self.copy_act.isEnabled(active) @pyqtSlot(bool) def setPasteActive(self, active): """Set whether the paste action is available :param active: Variable representing whether paste action should be set to available or unavailable :type active: bool """ self.paste_act.isEnabled(active) def selectFile(self): """Open file action to hand file handle to GameData """ file_name, _ = QFileDialog.getOpenFileName( self, "Open file", "", "JCAP Resource File (*.jrf)") self.loadProject(file_name) def loadProject(self, file_name): """Load project file data and populate UI elements/set up signals and slots """ if file_name: try: self.data = GameData.fromFilename(file_name, self) except KeyError: QMessageBox( QMessageBox.Critical, "Error", "Unable to load project due to malformed data", ).exec() return except OSError: QMessageBox( QMessageBox.Critical, "Error", "Unable to open project file", ).exec() return else: return self.setWindowTitle("JIDE - " + self.data.getGameName()) self.gendat_act.setEnabled(True) self.load_jcap.setEnabled(True) self.data.setUndoStack(self.undo_stack) self.sprite_scene = GraphicsScene(self.data, Source.SPRITE, self) self.tile_scene = GraphicsScene(self.data, Source.TILE, self) self.sprite_view = GraphicsView(self.sprite_scene, self) self.tile_view = GraphicsView(self.tile_scene, self) self.sprite_view.setStyleSheet("background-color: #494949;") self.tile_view.setStyleSheet("background-color: #494949;") sprite_pixel_palette = self.sprite_pixel_palette_dock.pixel_palette tile_pixel_palette = self.tile_pixel_palette_dock.pixel_palette sprite_color_palette = self.sprite_color_palette_dock.color_palette tile_color_palette = self.tile_color_palette_dock.color_palette sprite_pixel_palette.subject_selected.connect( self.sprite_scene.setSubject) self.sprite_scene.set_color_switch_enabled.connect( sprite_color_palette.color_preview.setColorSwitchEnabled) self.sprite_color_palette_dock.palette_updated.connect( self.sprite_scene.setColorPalette) self.sprite_color_palette_dock.palette_updated.connect( self.sprite_pixel_palette_dock.palette_updated) sprite_color_palette.color_selected.connect( self.sprite_scene.setPrimaryColor) tile_pixel_palette.subject_selected.connect(self.tile_scene.setSubject) self.tile_scene.set_color_switch_enabled.connect( tile_color_palette.color_preview.setColorSwitchEnabled) self.tile_color_palette_dock.palette_updated.connect( self.tile_scene.setColorPalette) self.tile_color_palette_dock.palette_updated.connect( self.tile_pixel_palette_dock.palette_updated) tile_color_palette.color_selected.connect( self.tile_scene.setPrimaryColor) self.sprite_color_palette_dock.setup(self.data) self.tile_color_palette_dock.setup(self.data) self.sprite_pixel_palette_dock.setup(self.data) self.tile_pixel_palette_dock.setup(self.data) self.canvas_tabs = QTabWidget() self.canvas_tabs.addTab(self.sprite_view, "Sprites") self.canvas_tabs.addTab(self.tile_view, "Tiles") self.canvas_tabs.setTabEnabled(0, True) self.canvas_tabs.setTabEnabled(1, True) self.setCentralWidget(self.canvas_tabs) self.canvas_tabs.currentChanged.connect(self.setCanvas) self.setCanvas(0) self.data.col_pal_updated.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.data.col_pal_renamed.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.data.col_pal_added.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.data.col_pal_removed.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.data.pix_batch_updated.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.data.row_count_updated.connect( lambda source, *_: self.canvas_tabs.setCurrentIndex(int(source))) self.select_tool.triggered.connect( lambda checked, tool=Tools.SELECT: self.sprite_scene.setTool(tool)) self.select_tool.triggered.connect( lambda checked, tool=Tools.SELECT: self.tile_scene.setTool(tool)) self.pen_tool.triggered.connect( lambda checked, tool=Tools.PEN: self.sprite_scene.setTool(tool)) self.pen_tool.triggered.connect( lambda checked, tool=Tools.PEN: self.tile_scene.setTool(tool)) self.fill_tool.triggered.connect(lambda checked, tool=Tools.FLOODFILL: self.sprite_scene.setTool(tool)) self.fill_tool.triggered.connect(lambda checked, tool=Tools.FLOODFILL: self.tile_scene.setTool(tool)) self.line_tool.triggered.connect( lambda checked, tool=Tools.LINE: self.sprite_scene.setTool(tool)) self.line_tool.triggered.connect( lambda checked, tool=Tools.LINE: self.tile_scene.setTool(tool)) self.rect_tool.triggered.connect(lambda checked, tool=Tools.RECTANGLE: self.sprite_scene.setTool(tool)) self.rect_tool.triggered.connect(lambda checked, tool=Tools.RECTANGLE: self.tile_scene.setTool(tool)) self.ellipse_tool.triggered.connect(lambda checked, tool=Tools.ELLIPSE: self.sprite_scene.setTool(tool)) self.ellipse_tool.triggered.connect( lambda checked, tool=Tools.ELLIPSE: self.tile_scene.setTool(tool)) for tool in self.tools: tool.setEnabled(True) self.pen_tool.setChecked(True) self.pen_tool.triggered.emit(True) def setCanvas(self, index): """Set the dock and signal/slot layout to switch between sprite/tile/ tile map tabs :param index: Index of canvas tab :type index: int """ self.paste_act.triggered.disconnect() self.copy_act.triggered.disconnect() if index == 0: self.copy_act.triggered.connect(self.sprite_scene.copy) self.paste_act.triggered.connect(self.sprite_scene.startPasting) self.tile_color_palette_dock.hide() self.tile_pixel_palette_dock.hide() self.sprite_color_palette_dock.show() self.sprite_pixel_palette_dock.show() self.removeDockWidget(self.tile_color_palette_dock) self.removeDockWidget(self.tile_pixel_palette_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.sprite_color_palette_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.sprite_pixel_palette_dock) self.copy_act.triggered.connect(self.sprite_scene.copy) self.paste_act.triggered.connect(self.sprite_scene.startPasting) self.sprite_scene.region_copied.connect(self.paste_act.setEnabled) self.sprite_scene.region_selected.connect(self.copy_act.setEnabled) elif index == 1: self.copy_act.triggered.connect(self.tile_scene.copy) self.paste_act.triggered.connect(self.tile_scene.startPasting) self.sprite_color_palette_dock.hide() self.sprite_pixel_palette_dock.hide() self.tile_color_palette_dock.show() self.tile_pixel_palette_dock.show() self.removeDockWidget(self.sprite_color_palette_dock) self.removeDockWidget(self.sprite_pixel_palette_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.tile_color_palette_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.tile_pixel_palette_dock) self.copy_act.triggered.connect(self.tile_scene.copy) self.paste_act.triggered.connect(self.tile_scene.startPasting) self.tile_scene.region_copied.connect(self.paste_act.setEnabled) self.tile_scene.region_selected.connect(self.copy_act.setEnabled) def openPrefs(self): prefs = Preferences() prefs.exec() def genDATFiles(self): """Generate .dat files from project for use by JCAP """ dat_path = Path(__file__).parents[1] / "data" / "DAT Files" dat_path.mkdir(exist_ok=True) tcp_path = dat_path / "tile_color_palettes.dat" tpp_path = dat_path / "tiles.dat" scp_path = dat_path / "sprite_color_palettes.dat" spp_path = dat_path / "sprites.dat" tile_pixel_data = self.data.getPixelPalettes(Source.TILE) tile_color_data = self.data.getColPals(Source.TILE) sprite_pixel_data = self.data.getPixelPalettes(Source.SPRITE) sprite_color_data = self.data.getColPals(Source.SPRITE) self.genPixelDATFile(tile_pixel_data, tpp_path) self.genColorDATFile(tile_color_data, tcp_path) self.genPixelDATFile(sprite_pixel_data, spp_path) self.genColorDATFile(sprite_color_data, scp_path) def genPixelDATFile(self, source, path): """Generate sprite/tile pixel palette .dat file :param source: List containing sprite/tile pixel data :type source: list :param path: File path to .dat :type path: str """ with path.open("wb") as dat_file: for element in source: for line in element: total = 0 for pixel in line: total = (total << 4) + pixel dat_file.write(total.to_bytes(4, byteorder="big")[::-1]) def genColorDATFile(self, source, path): """Generate sprite/tile color palette .dat file :param source: List containing sprite/tile color data :type source: list :param path: File path to .dat :type path: str """ with path.open("wb") as dat_file: for palette in source: for color in palette: r, g, b = downsample(color.red(), color.green(), color.blue()) rgb = (r << 5) | (g << 2) | (b) dat_file.write(bytes([rgb])) def loadJCAP(self): """Generate .dat files and execute command-line serial loading of JCAP """ self.statusBar.showMessage("Loading JCAP...") self.genDATFiles() dat_path = Path(__file__).parents[1] / "data" / "DAT Files" jcap_path = Path(__file__).parents[2] / "jcap" / "dev" / "software" sysload_path = jcap_path / "sysload.sh" for dat_file in dat_path.glob("**/*"): shutil.copy(str(dat_file), str(jcap_path)) self.prefs.beginGroup("ports") if not self.prefs.contains("cpu_port") or not self.prefs.contains( "gpu_port"): # Popup error self.openPrefs() return cpu_port = self.prefs.value("cpu_port") gpu_port = self.prefs.value("gpu_port") self.prefs.endGroup() result = subprocess.run( ["bash.exe", str(sysload_path), "-c", cpu_port, "-g", gpu_port], capture_output=True, ) print(result.stderr) self.statusBar.showMessage("JCAP Loaded!", 5000)
class VidCutter(QWidget): def __init__(self, parent): super(VidCutter, self).__init__(parent) self.novideoWidget = QWidget(self, autoFillBackground=True) self.parent = parent self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.VideoSurface) self.videoWidget = VideoWidget(self) self.videoService = VideoService(self) QFontDatabase.addApplicationFont( MainWindow.get_path('fonts/DroidSansMono.ttf')) QFontDatabase.addApplicationFont( MainWindow.get_path('fonts/OpenSans.ttf')) fontSize = 12 if sys.platform == 'darwin' else 10 appFont = QFont('Open Sans', fontSize, 300) qApp.setFont(appFont) self.clipTimes = [] self.inCut = False self.movieFilename = '' self.movieLoaded = False self.timeformat = 'hh:mm:ss' self.finalFilename = '' self.totalRuntime = 0 self.initIcons() self.initActions() self.toolbar = QToolBar(floatable=False, movable=False, iconSize=QSize(40, 36)) self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.toolbar.setStyleSheet('''QToolBar { spacing:10px; } QToolBar QToolButton { border:1px solid transparent; min-width:95px; font-size:11pt; font-weight:400; border-radius:5px; padding:1px 2px; color:#444; } QToolBar QToolButton:hover { border:1px inset #6A4572; color:#6A4572; background-color:rgba(255, 255, 255, 0.85); } QToolBar QToolButton:pressed { border:1px inset #6A4572; color:#6A4572; background-color:rgba(255, 255, 255, 0.25); } QToolBar QToolButton:disabled { color:#999; }''') self.initToolbar() self.appMenu, self.cliplistMenu = QMenu(), QMenu() self.initMenus() self.seekSlider = VideoSlider(parent=self, sliderMoved=self.setPosition) self.initNoVideo() self.cliplist = QListWidget( sizePolicy=QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding), contextMenuPolicy=Qt.CustomContextMenu, uniformItemSizes=True, iconSize=QSize(100, 700), dragDropMode=QAbstractItemView.InternalMove, alternatingRowColors=True, customContextMenuRequested=self.itemMenu, dragEnabled=True) self.cliplist.setStyleSheet( 'QListView { border-radius:0; border:none; border-left:1px solid #B9B9B9; ' + 'border-right:1px solid #B9B9B9; } QListView::item { padding:10px 0; }' ) self.cliplist.setFixedWidth(185) self.cliplist.model().rowsMoved.connect(self.syncClipList) listHeader = QLabel(pixmap=QPixmap( MainWindow.get_path('images/clipindex.png'), 'PNG'), alignment=Qt.AlignCenter) listHeader.setStyleSheet( '''padding:5px; padding-top:8px; border:1px solid #b9b9b9; background-color:qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #FFF, stop: 0.5 #EAEAEA, stop: 0.6 #EAEAEA stop:1 #FFF);''' ) self.runtimeLabel = QLabel('<div align="right">00:00:00</div>', textFormat=Qt.RichText) self.runtimeLabel.setStyleSheet( '''font-family:Droid Sans Mono; font-size:10pt; color:#FFF; background:rgb(106, 69, 114) url(:images/runtime.png) no-repeat left center; padding:2px; padding-right:8px; border:1px solid #B9B9B9;''') self.clipindexLayout = QVBoxLayout(spacing=0) self.clipindexLayout.setContentsMargins(0, 0, 0, 0) self.clipindexLayout.addWidget(listHeader) self.clipindexLayout.addWidget(self.cliplist) self.clipindexLayout.addWidget(self.runtimeLabel) self.videoLayout = QHBoxLayout() self.videoLayout.setContentsMargins(0, 0, 0, 0) self.videoLayout.addWidget(self.novideoWidget) self.videoLayout.addLayout(self.clipindexLayout) self.timeCounter = QLabel('00:00:00 / 00:00:00', autoFillBackground=True, alignment=Qt.AlignCenter, sizePolicy=QSizePolicy( QSizePolicy.Expanding, QSizePolicy.Fixed)) self.timeCounter.setStyleSheet( 'color:#FFF; background:#000; font-family:Droid Sans Mono; font-size:10.5pt; padding:4px;' ) videoplayerLayout = QVBoxLayout(spacing=0) videoplayerLayout.setContentsMargins(0, 0, 0, 0) videoplayerLayout.addWidget(self.videoWidget) videoplayerLayout.addWidget(self.timeCounter) self.videoplayerWidget = QWidget(self, visible=False) self.videoplayerWidget.setLayout(videoplayerLayout) self.muteButton = QPushButton(icon=self.unmuteIcon, flat=True, toolTip='Mute', statusTip='Toggle audio mute', iconSize=QSize(16, 16), cursor=Qt.PointingHandCursor, clicked=self.muteAudio) self.volumeSlider = QSlider(Qt.Horizontal, toolTip='Volume', statusTip='Adjust volume level', cursor=Qt.PointingHandCursor, value=50, minimum=0, maximum=100, sliderMoved=self.setVolume) self.menuButton = QPushButton( icon=self.menuIcon, flat=True, toolTip='Menu', statusTip='Media + application information', iconSize=QSize(24, 24), cursor=Qt.PointingHandCursor) self.menuButton.setMenu(self.appMenu) toolbarLayout = QHBoxLayout() toolbarLayout.addWidget(self.toolbar) toolbarLayout.setContentsMargins(2, 2, 2, 2) toolbarGroup = QGroupBox() toolbarGroup.setFlat(False) toolbarGroup.setCursor(Qt.PointingHandCursor) toolbarGroup.setLayout(toolbarLayout) toolbarGroup.setStyleSheet( '''QGroupBox { background-color:rgba(0, 0, 0, 0.1); border:1px inset #888; border-radius:5px; }''') controlsLayout = QHBoxLayout(spacing=0) controlsLayout.addStretch(1) controlsLayout.addWidget(toolbarGroup) controlsLayout.addStretch(1) controlsLayout.addWidget(self.muteButton) controlsLayout.addWidget(self.volumeSlider) controlsLayout.addSpacing(1) controlsLayout.addWidget(self.menuButton) layout = QVBoxLayout() layout.setContentsMargins(10, 10, 10, 4) layout.addLayout(self.videoLayout) layout.addWidget(self.seekSlider) layout.addSpacing(5) layout.addLayout(controlsLayout) layout.addSpacing(2) self.setLayout(layout) self.mediaPlayer.setVideoOutput(self.videoWidget) self.mediaPlayer.stateChanged.connect(self.mediaStateChanged) self.mediaPlayer.positionChanged.connect(self.positionChanged) self.mediaPlayer.durationChanged.connect(self.durationChanged) self.mediaPlayer.error.connect(self.handleError) def initNoVideo(self) -> None: novideoImage = QLabel( alignment=Qt.AlignCenter, autoFillBackground=False, pixmap=QPixmap(MainWindow.get_path('images/novideo.png'), 'PNG'), sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)) novideoImage.setBackgroundRole(QPalette.Dark) novideoImage.setContentsMargins(0, 20, 0, 20) self.novideoLabel = QLabel(alignment=Qt.AlignCenter, autoFillBackground=True, sizePolicy=QSizePolicy( QSizePolicy.Expanding, QSizePolicy.Minimum)) self.novideoLabel.setBackgroundRole(QPalette.Dark) self.novideoLabel.setContentsMargins(0, 20, 15, 60) novideoLayout = QVBoxLayout(spacing=0) novideoLayout.addWidget(novideoImage) novideoLayout.addWidget(self.novideoLabel, alignment=Qt.AlignTop) self.novideoMovie = QMovie( MainWindow.get_path('images/novideotext.gif')) self.novideoMovie.frameChanged.connect(self.setNoVideoText) self.novideoMovie.start() self.novideoWidget.setBackgroundRole(QPalette.Dark) self.novideoWidget.setLayout(novideoLayout) def initIcons(self) -> None: self.appIcon = QIcon(MainWindow.get_path('images/vidcutter.png')) self.openIcon = icon('fa.film', color='#444', color_active='#6A4572', scale_factor=0.9) self.playIcon = icon('fa.play-circle-o', color='#444', color_active='#6A4572', scale_factor=1.1) self.pauseIcon = icon('fa.pause-circle-o', color='#444', color_active='#6A4572', scale_factor=1.1) self.cutStartIcon = icon('fa.scissors', scale_factor=1.15, color='#444', color_active='#6A4572') endicon_normal = icon('fa.scissors', scale_factor=1.15, color='#444').pixmap(QSize(36, 36)).toImage() endicon_active = icon('fa.scissors', scale_factor=1.15, color='#6A4572').pixmap(QSize(36, 36)).toImage() self.cutEndIcon = QIcon() self.cutEndIcon.addPixmap( QPixmap.fromImage( endicon_normal.mirrored(horizontal=True, vertical=False)), QIcon.Normal, QIcon.Off) self.cutEndIcon.addPixmap( QPixmap.fromImage( endicon_active.mirrored(horizontal=True, vertical=False)), QIcon.Active, QIcon.Off) self.saveIcon = icon('fa.video-camera', color='#6A4572', color_active='#6A4572') self.muteIcon = QIcon(MainWindow.get_path('images/muted.png')) self.unmuteIcon = QIcon(MainWindow.get_path('images/unmuted.png')) self.upIcon = icon('ei.caret-up', color='#444') self.downIcon = icon('ei.caret-down', color='#444') self.removeIcon = icon('ei.remove', color='#B41D1D') self.removeAllIcon = icon('ei.trash', color='#B41D1D') self.successIcon = QIcon(MainWindow.get_path('images/success.png')) self.menuIcon = icon('fa.cog', color='#444', scale_factor=1.15) self.completePlayIcon = icon('fa.play', color='#444') self.completeOpenIcon = icon('fa.folder-open', color='#444') self.completeRestartIcon = icon('fa.retweet', color='#444') self.completeExitIcon = icon('fa.sign-out', color='#444') self.mediaInfoIcon = icon('fa.info-circle', color='#444') self.updateCheckIcon = icon('fa.cloud-download', color='#444') def initActions(self) -> None: self.openAction = QAction(self.openIcon, 'Open', self, statusTip='Open media file', triggered=self.openMedia) self.playAction = QAction(self.playIcon, 'Play', self, statusTip='Play media file', triggered=self.playMedia, enabled=False) self.cutStartAction = QAction(self.cutStartIcon, ' Start', self, toolTip='Start', statusTip='Set clip start marker', triggered=self.setCutStart, enabled=False) self.cutEndAction = QAction(self.cutEndIcon, ' End', self, toolTip='End', statusTip='Set clip end marker', triggered=self.setCutEnd, enabled=False) self.saveAction = QAction(self.saveIcon, 'Save', self, statusTip='Save clips to a new video file', triggered=self.cutVideo, enabled=False) self.moveItemUpAction = QAction( self.upIcon, 'Move up', self, statusTip='Move clip position up in list', triggered=self.moveItemUp, enabled=False) self.moveItemDownAction = QAction( self.downIcon, 'Move down', self, statusTip='Move clip position down in list', triggered=self.moveItemDown, enabled=False) self.removeItemAction = QAction( self.removeIcon, 'Remove clip', self, statusTip='Remove selected clip from list', triggered=self.removeItem, enabled=False) self.removeAllAction = QAction(self.removeAllIcon, 'Clear list', self, statusTip='Clear all clips from list', triggered=self.clearList, enabled=False) self.mediaInfoAction = QAction( self.mediaInfoIcon, 'Media information', self, statusTip='View current media file information', triggered=self.mediaInfo, enabled=False) self.updateCheckAction = QAction( self.updateCheckIcon, 'Check for updates...', self, statusTip='Check for application updates', triggered=self.updateCheck) self.aboutQtAction = QAction('About Qt', self, statusTip='About Qt', triggered=qApp.aboutQt) self.aboutAction = QAction('About %s' % qApp.applicationName(), self, statusTip='Credits and licensing', triggered=self.aboutInfo) def initToolbar(self) -> None: self.toolbar.addAction(self.openAction) self.toolbar.addAction(self.playAction) self.toolbar.addAction(self.cutStartAction) self.toolbar.addAction(self.cutEndAction) self.toolbar.addAction(self.saveAction) def initMenus(self) -> None: self.appMenu.addAction(self.mediaInfoAction) self.appMenu.addAction(self.updateCheckAction) self.appMenu.addSeparator() self.appMenu.addAction(self.aboutQtAction) self.appMenu.addAction(self.aboutAction) self.cliplistMenu.addAction(self.moveItemUpAction) self.cliplistMenu.addAction(self.moveItemDownAction) self.cliplistMenu.addSeparator() self.cliplistMenu.addAction(self.removeItemAction) self.cliplistMenu.addAction(self.removeAllAction) @staticmethod def getSpacer() -> QWidget: spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) return spacer def setRunningTime(self, runtime: str) -> None: self.runtimeLabel.setText('<div align="right">%s</div>' % runtime) @pyqtSlot(int) def setNoVideoText(self) -> None: self.novideoLabel.setPixmap(self.novideoMovie.currentPixmap()) def itemMenu(self, pos: QPoint) -> None: globalPos = self.cliplist.mapToGlobal(pos) self.moveItemUpAction.setEnabled(False) self.moveItemDownAction.setEnabled(False) self.removeItemAction.setEnabled(False) self.removeAllAction.setEnabled(False) index = self.cliplist.currentRow() if index != -1: if not self.inCut: if index > 0: self.moveItemUpAction.setEnabled(True) if index < self.cliplist.count() - 1: self.moveItemDownAction.setEnabled(True) if self.cliplist.count() > 0: self.removeItemAction.setEnabled(True) if self.cliplist.count() > 0: self.removeAllAction.setEnabled(True) self.cliplistMenu.exec_(globalPos) def moveItemUp(self) -> None: index = self.cliplist.currentRow() tmpItem = self.clipTimes[index] del self.clipTimes[index] self.clipTimes.insert(index - 1, tmpItem) self.renderTimes() def moveItemDown(self) -> None: index = self.cliplist.currentRow() tmpItem = self.clipTimes[index] del self.clipTimes[index] self.clipTimes.insert(index + 1, tmpItem) self.renderTimes() def removeItem(self) -> None: index = self.cliplist.currentRow() del self.clipTimes[index] if self.inCut and index == self.cliplist.count() - 1: self.inCut = False self.initMediaControls() self.renderTimes() def clearList(self) -> None: self.clipTimes.clear() self.cliplist.clear() self.inCut = False self.renderTimes() self.initMediaControls() def mediaInfo(self) -> None: if self.mediaPlayer.isMetaDataAvailable(): content = '<table cellpadding="4">' for key in self.mediaPlayer.availableMetaData(): val = self.mediaPlayer.metaData(key) if type(val) is QSize: val = '%s x %s' % (val.width(), val.height()) content += '<tr><td align="right"><b>%s:</b></td><td>%s</td></tr>\n' % ( key, val) content += '</table>' mbox = QMessageBox(windowTitle='Media Information', windowIcon=self.parent.windowIcon(), textFormat=Qt.RichText) mbox.setText('<b>%s</b>' % os.path.basename( self.mediaPlayer.currentMedia().canonicalUrl().toLocalFile())) mbox.setInformativeText(content) mbox.exec_() else: QMessageBox.critical( self.parent, 'MEDIA ERROR', '<h3>Could not probe media file.</h3>' + '<p>An error occurred while analyzing the media file for its metadata details.' + '<br/><br/><b>This DOES NOT mean there is a problem with the file and you should ' + 'be able to continue using it.</b></p>') def aboutInfo(self) -> None: about_html = '''<style> a { color:#441d4e; text-decoration:none; font-weight:bold; } a:hover { text-decoration:underline; } </style> <div style="min-width:650px;"> <p style="font-size:26pt; font-weight:bold; color:#6A4572;">%s</p> <p> <span style="font-size:13pt;"><b>Version: %s</b></span> <span style="font-size:10pt;position:relative;left:5px;">( %s )</span> </p> <p style="font-size:13px;"> Copyright © 2016 <a href="mailto:[email protected]">Pete Alexandrou</a> <br/> Website: <a href="%s">%s</a> </p> <p style="font-size:13px;"> Thanks to the folks behind the <b>Qt</b>, <b>PyQt</b> and <b>FFmpeg</b> projects for all their hard and much appreciated work. </p> <p style="font-size:11px;"> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. </p> <p style="font-size:11px;"> This software uses libraries from the <a href="https://www.ffmpeg.org">FFmpeg</a> project under the <a href="https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html">LGPLv2.1</a> </p></div>''' % (qApp.applicationName(), qApp.applicationVersion(), platform.architecture()[0], qApp.organizationDomain(), qApp.organizationDomain()) QMessageBox.about(self.parent, 'About %s' % qApp.applicationName(), about_html) def openMedia(self) -> None: filename, _ = QFileDialog.getOpenFileName(self.parent, caption='Select video', directory=QDir.homePath()) if filename != '': self.loadFile(filename) def loadFile(self, filename: str) -> None: self.movieFilename = filename if not os.path.exists(filename): return self.mediaPlayer.setMedia(QMediaContent(QUrl.fromLocalFile(filename))) self.initMediaControls(True) self.cliplist.clear() self.clipTimes = [] self.parent.setWindowTitle( '%s - %s' % (qApp.applicationName(), os.path.basename(filename))) if not self.movieLoaded: self.videoLayout.replaceWidget(self.novideoWidget, self.videoplayerWidget) self.novideoMovie.stop() self.novideoMovie.deleteLater() self.novideoWidget.deleteLater() self.videoplayerWidget.show() self.videoWidget.show() self.movieLoaded = True if self.mediaPlayer.isVideoAvailable(): self.mediaPlayer.setPosition(1) self.mediaPlayer.play() self.mediaPlayer.pause() def playMedia(self) -> None: if self.mediaPlayer.state() == QMediaPlayer.PlayingState: self.mediaPlayer.pause() self.playAction.setText('Play') else: self.mediaPlayer.play() self.playAction.setText('Pause') def initMediaControls(self, flag: bool = True) -> None: self.playAction.setEnabled(flag) self.saveAction.setEnabled(False) self.cutStartAction.setEnabled(flag) self.cutEndAction.setEnabled(False) self.mediaInfoAction.setEnabled(flag) if flag: self.seekSlider.setRestrictValue(0) def setPosition(self, position: int) -> None: self.mediaPlayer.setPosition(position) def positionChanged(self, progress: int) -> None: self.seekSlider.setValue(progress) currentTime = self.deltaToQTime(progress) totalTime = self.deltaToQTime(self.mediaPlayer.duration()) self.timeCounter.setText('%s / %s' % (currentTime.toString( self.timeformat), totalTime.toString(self.timeformat))) @pyqtSlot() def mediaStateChanged(self) -> None: if self.mediaPlayer.state() == QMediaPlayer.PlayingState: self.playAction.setIcon(self.pauseIcon) else: self.playAction.setIcon(self.playIcon) def durationChanged(self, duration: int) -> None: self.seekSlider.setRange(0, duration) def muteAudio(self) -> None: if self.mediaPlayer.isMuted(): self.muteButton.setIcon(self.unmuteIcon) self.muteButton.setToolTip('Mute') else: self.muteButton.setIcon(self.muteIcon) self.muteButton.setToolTip('Unmute') self.mediaPlayer.setMuted(not self.mediaPlayer.isMuted()) def setVolume(self, volume: int) -> None: self.mediaPlayer.setVolume(volume) def toggleFullscreen(self) -> None: self.videoWidget.setFullScreen(not self.videoWidget.isFullScreen()) def setCutStart(self) -> None: self.clipTimes.append([ self.deltaToQTime(self.mediaPlayer.position()), '', self.captureImage() ]) self.cutStartAction.setDisabled(True) self.cutEndAction.setEnabled(True) self.seekSlider.setRestrictValue(self.seekSlider.value(), True) self.inCut = True self.renderTimes() def setCutEnd(self) -> None: item = self.clipTimes[len(self.clipTimes) - 1] selected = self.deltaToQTime(self.mediaPlayer.position()) if selected.__lt__(item[0]): QMessageBox.critical( self.parent, 'Invalid END Time', 'The clip end time must come AFTER it\'s start time. Please try again.' ) return item[1] = selected self.cutStartAction.setEnabled(True) self.cutEndAction.setDisabled(True) self.seekSlider.setRestrictValue(0, False) self.inCut = False self.renderTimes() @pyqtSlot(QModelIndex, int, int, QModelIndex, int) def syncClipList(self, parent: QModelIndex, start: int, end: int, destination: QModelIndex, row: int) -> None: if start < row: index = row - 1 else: index = row clip = self.clipTimes.pop(start) self.clipTimes.insert(index, clip) def renderTimes(self) -> None: self.cliplist.clear() if len(self.clipTimes) > 4: self.cliplist.setFixedWidth(200) else: self.cliplist.setFixedWidth(185) self.totalRuntime = 0 for item in self.clipTimes: endItem = '' if type(item[1]) is QTime: endItem = item[1].toString(self.timeformat) self.totalRuntime += item[0].msecsTo(item[1]) listitem = QListWidgetItem() listitem.setTextAlignment(Qt.AlignVCenter) if type(item[2]) is QPixmap: listitem.setIcon(QIcon(item[2])) self.cliplist.addItem(listitem) marker = QLabel( '''<style>b { font-size:7pt; } p { margin:2px 5px; }</style> <p><b>START</b><br/>%s<br/><b>END</b><br/>%s</p>''' % (item[0].toString(self.timeformat), endItem)) marker.setStyleSheet('border:none;') self.cliplist.setItemWidget(listitem, marker) listitem.setFlags(Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsEnabled) if len(self.clipTimes) and not self.inCut: self.saveAction.setEnabled(True) if self.inCut or len(self.clipTimes) == 0 or not type( self.clipTimes[0][1]) is QTime: self.saveAction.setEnabled(False) self.setRunningTime( self.deltaToQTime(self.totalRuntime).toString(self.timeformat)) @staticmethod def deltaToQTime(millisecs: int) -> QTime: secs = millisecs / 1000 return QTime((secs / 3600) % 60, (secs / 60) % 60, secs % 60, (secs * 1000) % 1000) def captureImage(self) -> QPixmap: frametime = self.deltaToQTime(self.mediaPlayer.position()).toString( self.timeformat) inputfile = self.mediaPlayer.currentMedia().canonicalUrl().toLocalFile( ) imagecap = self.videoService.capture(inputfile, frametime) if type(imagecap) is QPixmap: return imagecap def cutVideo(self) -> bool: clips = len(self.clipTimes) filename, filelist = '', [] source = self.mediaPlayer.currentMedia().canonicalUrl().toLocalFile() _, sourceext = os.path.splitext(source) if clips > 0: self.finalFilename, _ = QFileDialog.getSaveFileName( self.parent, 'Save video', source, 'Video files (*%s)' % sourceext) if self.finalFilename == '': return False qApp.setOverrideCursor(Qt.BusyCursor) self.saveAction.setDisabled(True) self.showProgress(clips) file, ext = os.path.splitext(self.finalFilename) index = 1 self.progress.setLabelText('Cutting media files...') qApp.processEvents() for clip in self.clipTimes: duration = self.deltaToQTime(clip[0].msecsTo( clip[1])).toString(self.timeformat) filename = '%s_%s%s' % (file, '{0:0>2}'.format(index), ext) filelist.append(filename) self.videoService.cut(source, filename, clip[0].toString(self.timeformat), duration) index += 1 if len(filelist) > 1: self.joinVideos(filelist, self.finalFilename) else: QFile.remove(self.finalFilename) QFile.rename(filename, self.finalFilename) self.progress.setLabelText('Complete...') self.progress.setValue(100) qApp.processEvents() self.progress.close() self.progress.deleteLater() qApp.restoreOverrideCursor() self.complete() return True return False def joinVideos(self, joinlist: list, filename: str) -> None: listfile = os.path.normpath( os.path.join(os.path.dirname(joinlist[0]), '.vidcutter.list')) fobj = open(listfile, 'w') for file in joinlist: fobj.write('file \'%s\'\n' % file.replace("'", "\\'")) fobj.close() self.videoService.join(listfile, filename) QFile.remove(listfile) for file in joinlist: if os.path.isfile(file): QFile.remove(file) def updateCheck(self) -> None: self.updater = Updater() self.updater.updateAvailable.connect(self.updateHandler) self.updater.start() def updateHandler(self, updateExists: bool, version: str = None): if updateExists: if Updater.notify_update(self, version) == QMessageBox.AcceptRole: self.updater.install_update(self) else: Updater.notify_no_update(self) def showProgress(self, steps: int, label: str = 'Analyzing media...') -> None: self.progress = QProgressDialog(label, None, 0, steps, self.parent, windowModality=Qt.ApplicationModal, windowIcon=self.parent.windowIcon(), minimumDuration=0, minimumWidth=500) self.progress.show() for i in range(steps): self.progress.setValue(i) qApp.processEvents() time.sleep(1) def complete(self) -> None: info = QFileInfo(self.finalFilename) mbox = QMessageBox(windowTitle='VIDEO PROCESSING COMPLETE', minimumWidth=500, textFormat=Qt.RichText) mbox.setText( ''' <style> table.info { margin:6px; padding:4px 15px; } td.label { font-weight:bold; font-size:10.5pt; text-align:right; } td.value { font-size:10.5pt; } </style> <table class="info" cellpadding="4" cellspacing="0"> <tr> <td class="label"><b>File:</b></td> <td class="value" nowrap>%s</td> </tr> <tr> <td class="label"><b>Size:</b></td> <td class="value">%s</td> </tr> <tr> <td class="label"><b>Length:</b></td> <td class="value">%s</td> </tr> </table><br/>''' % (QDir.toNativeSeparators( self.finalFilename), self.sizeof_fmt(int(info.size())), self.deltaToQTime(self.totalRuntime).toString(self.timeformat))) play = mbox.addButton('Play', QMessageBox.AcceptRole) play.setIcon(self.completePlayIcon) play.clicked.connect(self.openResult) fileman = mbox.addButton('Open', QMessageBox.AcceptRole) fileman.setIcon(self.completeOpenIcon) fileman.clicked.connect(self.openFolder) end = mbox.addButton('Exit', QMessageBox.AcceptRole) end.setIcon(self.completeExitIcon) end.clicked.connect(self.close) new = mbox.addButton('Restart', QMessageBox.AcceptRole) new.setIcon(self.completeRestartIcon) new.clicked.connect(self.parent.restart) mbox.setDefaultButton(new) mbox.setEscapeButton(new) mbox.adjustSize() mbox.exec_() def sizeof_fmt(self, num: float, suffix: chr = 'B') -> str: for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: if abs(num) < 1024.0: return "%3.1f%s%s" % (num, unit, suffix) num /= 1024.0 return "%.1f%s%s" % (num, 'Y', suffix) @pyqtSlot() def openFolder(self) -> None: self.openResult(pathonly=True) @pyqtSlot(bool) def openResult(self, pathonly: bool = False) -> None: self.parent.restart() if len(self.finalFilename) and os.path.exists(self.finalFilename): target = self.finalFilename if not pathonly else os.path.dirname( self.finalFilename) QDesktopServices.openUrl(QUrl.fromLocalFile(target)) @pyqtSlot() def startNew(self) -> None: qApp.restoreOverrideCursor() self.clearList() self.seekSlider.setValue(0) self.seekSlider.setRange(0, 0) self.mediaPlayer.setMedia(QMediaContent()) self.initNoVideo() self.videoLayout.replaceWidget(self.videoplayerWidget, self.novideoWidget) self.initMediaControls(False) self.parent.setWindowTitle('%s' % qApp.applicationName()) def wheelEvent(self, event: QWheelEvent) -> None: if self.mediaPlayer.isVideoAvailable( ) or self.mediaPlayer.isAudioAvailable(): if event.angleDelta().y() > 0: newval = self.seekSlider.value() - 1000 else: newval = self.seekSlider.value() + 1000 self.seekSlider.setValue(newval) self.seekSlider.setSliderPosition(newval) self.mediaPlayer.setPosition(newval) event.accept() def keyPressEvent(self, event: QKeyEvent) -> None: if self.mediaPlayer.isVideoAvailable( ) or self.mediaPlayer.isAudioAvailable(): addtime = 0 if event.key() == Qt.Key_Left: addtime = -1000 elif event.key() == Qt.Key_PageUp or event.key() == Qt.Key_Up: addtime = -10000 elif event.key() == Qt.Key_Right: addtime = 1000 elif event.key() == Qt.Key_PageDown or event.key() == Qt.Key_Down: addtime = 10000 elif event.key() == Qt.Key_Enter: self.toggleFullscreen() elif event.key( ) == Qt.Key_Escape and self.videoWidget.isFullScreen(): self.videoWidget.setFullScreen(False) if addtime != 0: newval = self.seekSlider.value() + addtime self.seekSlider.setValue(newval) self.seekSlider.setSliderPosition(newval) self.mediaPlayer.setPosition(newval) event.accept() def mousePressEvent(self, event: QMouseEvent) -> None: if event.button() == Qt.BackButton and self.cutStartAction.isEnabled(): self.setCutStart() event.accept() elif event.button( ) == Qt.ForwardButton and self.cutEndAction.isEnabled(): self.setCutEnd() event.accept() else: super(VidCutter, self).mousePressEvent(event) @pyqtSlot(QMediaPlayer.Error) def handleError(self, error: QMediaPlayer.Error) -> None: qApp.restoreOverrideCursor() self.startNew() if error == QMediaPlayer.ResourceError: QMessageBox.critical( self.parent, 'INVALID MEDIA', 'Invalid media file detected at:<br/><br/><b>%s</b><br/><br/>%s' % (self.movieFilename, self.mediaPlayer.errorString())) else: QMessageBox.critical(self.parent, 'ERROR NOTIFICATION', self.mediaPlayer.errorString()) def closeEvent(self, event: QCloseEvent) -> None: self.parent.closeEvent(event)