diff --git a/.travis.yml b/.travis.yml index 18ca3e87..f2f05655 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,293 @@ -language: python -python: - - "2.7" +# vim: set ts=2 et: -addons: - apt: - packages: - - cmake - - pyqt4-dev-tools +# run xvfb with 32-bit color +# xvfb-run -s '-screen 0 1600x1200x24+32' command_goes_here -# command to install dependencies -#install: "pip install --user -r requirements.txt" -# command to run tests -script: make all +matrix: + include: + + # Python 2.7 + QT4 + - os: linux + dist: trusty + sudo: required + language: generic + python: "2.7" + env: + - QT=4 + addons: + apt: + packages: + - cmake + - python-qt4 + - pyqt4-dev-tools + - xvfb + before_install: + - sudo pip install lxml + - make qt4py2 + - xvfb-run make testpy2 + + # Python 2.7 + QT4 + - os: linux + dist: trusty + sudo: required + language: generic + python: "2.7" + env: + - QT=4 + - CONDA=4.2.0 + addons: + apt: + packages: + - cmake + #- python-qt4 + #- pyqt4-dev-tools + - xvfb + before_install: + # ref: https://www.continuum.io/downloads + - curl -O https://repo.continuum.io/archive/Anaconda2-4.2.0-Linux-x86_64.sh + # ref: http://conda.pydata.org/docs/help/silent.html + - /bin/bash Anaconda2-4.2.0-Linux-x86_64.sh -b -p $HOME/anaconda2 + - export PATH="$HOME/anaconda2/bin:$PATH" + # ref: http://stackoverflow.com/questions/21637922/how-to-install-pyqt4-in-anaconda + - conda create -y -n labelImg-py2qt4 python=2.7 + - source activate labelImg-py2qt4 + - conda install -y pyqt=4 + - conda install -y lxml + - make qt4py2 + - xvfb-run make testpy2 + + # Python 2 + QT5 + # disabled; can't get it to work + #- os: linux + # dist: trusty + # sudo: required + # language: generic + # python: "2.7" + # env: + # - QT=5 + # addons: + # apt: + # packages: + # - cmake + # - pyqt5-dev-tools + # - xvfb + # before_install: + # - sudo apt-get update + # - sudo apt-get install -y python-pip + # - sudo pip install lxml + # - pyrcc5 --help || true # does QT5 support python2 out of the box? + # - make qt5py3 + # - xvfb-run make testpy2 + + # Python 3 + QT4 + - os: linux + dist: trusty + sudo: required + language: generic + python: "3.5" + env: + - QT=4 + addons: + apt: + packages: + - cmake + - python3-pyqt4 + - pyqt4-dev-tools + - xvfb + before_install: + - sudo apt-get update + - sudo apt-get install -y python3-pip + - sudo pip3 install lxml + - make qt4py3 + - xvfb-run make testpy3 + + # Python 3 + QT5 + - os: linux + dist: trusty + sudo: required + language: generic + python: "3.5" + env: + - QT=5 + addons: + apt: + packages: + - cmake + - pyqt5-dev-tools + - xvfb + before_install: + - sudo apt-get update + - sudo apt-get install -y python3-pip + - sudo pip3 install lxml + - make qt5py3 + - xvfb-run make testpy3 + + # Python 3 + QT5 + - os: linux + dist: trusty + sudo: required + language: generic + python: "3.5" + env: + - QT=5 + - CONDA=4.2.0 + addons: + apt: + packages: + - cmake + - xvfb + before_install: + # ref: https://www.continuum.io/downloads + - curl -O https://repo.continuum.io/archive/Anaconda3-4.2.0-Linux-x86_64.sh + # ref: http://conda.pydata.org/docs/help/silent.html + - /bin/bash Anaconda3-4.2.0-Linux-x86_64.sh -b -p $HOME/anaconda3 + - export PATH="$HOME/anaconda3/bin:$PATH" + # ref: http://stackoverflow.com/questions/21637922/how-to-install-pyqt4-in-anaconda + - conda create -y -n labelImg-py3qt5 python=3.5 + - source activate labelImg-py3qt5 + - conda install -y pyqt=5 + - conda install -y lxml + - make qt5py3 + - xvfb-run make testpy3 + + # OS X 10.10 Python 3 + QT5 + - os: osx + osx_image: xcode6.4 # Xcode 6.4, OS X 10.10 + sudo: required + language: generic + python: "3.6" + env: + - QT=5 + before_install: + #- brew update + - brew install libxml2 + - brew install pyqt5 + - which python3 pip3 + - python3 --version + #- sudo -H pip3 install --user --upgrade lxml # pyqt5 installs python3.x, which installs pip3 + - sudo -H easy_install-3.6 lxml || true + - python3 -c 'import sys; print(sys.path)' + - python3 -c 'import lxml' + - make qt5py3 + - python3 -c 'help("modules")' + - make testpy3 # FIXME: does not work, segfault on travis-ci + + # OS X 10.11 Python 3 + QT5 + - os: osx + osx_image: xcode8 # Xcode 8, OS X 10.11 + sudo: required + language: generic + python: "3.6" + env: + - QT=5 + before_install: + #- brew update + - brew install libxml2 + - brew install pyqt5 + - which python3 pip3 + - python3 --version + #- sudo -H pip3 install --user --upgrade lxml # pyqt5 installs python3.x, which installs pip3 + - sudo -H easy_install-3.6 lxml || true + - python3 -c 'import sys; print(sys.path)' + - python3 -c 'import lxml' + - make qt5py3 + - python3 -c 'help("modules")' + - make testpy3 # FIXME: does not work, segfault on travis-ci + + # OS X 10.12 Python 3 + QT5 + - os: osx + osx_image: xcode8.2 # OS X 10.12 + sudo: required + language: generic + python: "3.6" + env: + - QT=5 + before_install: + #- brew update + - brew install libxml2 + - brew install pyqt5 + - which python3 pip3 + - python3 --version + #- sudo -H pip3 install --user --upgrade lxml # pyqt5 installs python3.x, which installs pip3 + - sudo -H easy_install-3.6 lxml || true + - python3 -c 'import sys; print(sys.path)' + - python3 -c 'import lxml' + - make qt5py3 + - python3 -c 'help("modules")' + #- make testpy3 # FIXME: does not work, segfault on travis-ci + # just make sure the app runs... :-/ + - ( python3 labelImg.py ) & sleep 10; kill $! + + # XXX: building QT4 from source takes forever... + + # OS X 10.11 Python 2 + QT4 + #- os: osx + # osx_image: xcode7.3 # OS X 10.11 + # sudo: required + # language: generic + # python: "2.7" + # env: + # - QT=4 + # before_install: + # - brew install libxml2 + # # build PyQT4... + # - curl -L -O https://sourceforge.net/projects/pyqt/files/sip/sip-4.19/sip-4.19.tar.gz + # - tar zxvf sip-4.19.tar.gz + # - (cd sip-4.19 && python configure.py -d /Library/Python/2.7/site-packages --arch x86_64 && make && sudo make install) + # # NOTE: produces insane amounts of output... + # - brew install -v cartr/qt4/qt --with-qt3support --without-webkit | sed -e's/ .* / ... /' + # - brew linkapps qt + # - curl -L -O http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.12/PyQt4_gpl_mac-4.12.tar.gz + # - tar zxvf PyQt4_gpl_mac-4.12.tar.gz + # - cd PyQt4_gpl_mac-4.12 + # - python configure.py --help + # - python configure.py -d /Library/Python/2.7/site-packages --use-arch=x86_64 --confirm-license + # - make + # - sudo make install + # - cd - + # - which python pip + # - python --version + # - sudo -H easy_install-2.7 lxml || true + # - python -c 'import sys; print(sys.path)' + # - python -c 'import lxml' + # - make qt4py2 + # - python -c 'help("modules")' + # - make testpy2 + + # OS X 10.11 Python 3 + QT4 + #- os: osx + # osx_image: xcode7.3 # OS X 10.11 + # sudo: required + # language: generic + # python: "3.6" + # env: + # - QT=4 + # before_install: + # - brew install libxml2 + # - brew install python3 + # - which python pip python3 pip3 || true + # - curl -L -O https://sourceforge.net/projects/pyqt/files/sip/sip-4.19/sip-4.19.tar.gz + # - tar zxvf sip-4.19.tar.gz + # - ( cd sip-4.19 && python3 configure.py -d /Library/Python/3.6/site-packages --arch x86_64 && make && sudo make install ) + # # NOTE: produces insane amounts of output... + # - brew install -v cartr/qt4/qt --with-qt3support --without-webkit | sed -e's/ .* / ... /' + # - brew linkapps qt + # - curl -L -O http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.12/PyQt4_gpl_mac-4.12.tar.gz + # - tar zxvf PyQt4_gpl_mac-4.12.tar.gz + # - cd PyQt4_gpl_mac-4.12 + # - python3 configure.py --help + # - python3 configure.py -d /Library/Python/3.6/site-packages --use-arch=x86_64 --confirm-license + # - make + # - sudo make install + # - cd - + # - which python3 pip3 + # - python3 --version + # - sudo -H easy_install-3.6 lxml || true + # - python3 -c 'import sys; print(sys.path)' + # - python3 -c 'import lxml' + # - make qt4py3 + # - python3 -c 'help("modules")' + # - make testpy3 + +script: + - exit 0 diff --git a/Makefile b/Makefile index 28aa5a63..5b6e4d8d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,26 @@ +# ex: set ts=8 noet: -all: resources.py +all: qt4 -%.py: %.qrc - pyrcc4 -o $@ $< +test: testpy2 +testpy2: + python -m unittest discover tests + +testpy3: + python3 -m unittest discover tests + +qt4: qt4py2 + +qt5: qt4py3 + +qt4py2: + pyrcc4 -py2 -o resources.py resources.qrc + +qt4py3: + pyrcc4 -py3 -o resources.py resources.qrc + +qt5py3: + pyrcc5 -o resources.py resources.qrc + +.PHONY: test diff --git a/labelImg.py b/labelImg.py index ab74772c..5fb3f021 100755 --- a/labelImg.py +++ b/labelImg.py @@ -10,8 +10,19 @@ import subprocess from functools import partial from collections import defaultdict -from PyQt4.QtGui import * -from PyQt4.QtCore import * +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * +except ImportError: + # needed for py3+qt4 + # ref: http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html + # ref: http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string + if sys.version_info.major >= 3: + import sip + sip.setapi('QVariant', 2) + from PyQt4.QtGui import * + from PyQt4.QtCore import * import resources @@ -29,6 +40,24 @@ __appname__ = 'labelImg' ### Utility functions and classes. + +def u(x): + '''py2/py3 unicode helper''' + try: + return x.decode('utf8') # py2 + except AttributeError: + return x # py3 + + +def have_qstring(): + '''p3/qt5 get rid of QString wrapper as py3 has native unicode str type''' + return not (sys.version_info.major >= 3 or QT_VERSION_STR.startswith('5.')) + + +def util_qt_strlistclass(): + return QStringList if have_qstring() else list + + class WindowMixin(object): def menu(self, title, actions=None): menu = self.menuBar().addMenu(title) @@ -47,8 +76,16 @@ class WindowMixin(object): return toolbar +# PyQt5: TypeError: unhashable type: 'QListWidgetItem' +class HashableQListWidgetItem(QListWidgetItem): + def __init__(self, *args): + super(HashableQListWidgetItem, self).__init__(*args) + def __hash__(self): + return hash(id(self)) + + class MainWindow(QMainWindow, WindowMixin): - FIT_WINDOW, FIT_WIDTH, MANUAL_ZOOM = range(3) + FIT_WINDOW, FIT_WIDTH, MANUAL_ZOOM = list(range(3)) def __init__(self, filename=None): super(MainWindow, self).__init__() @@ -323,39 +360,65 @@ class MainWindow(QMainWindow, WindowMixin): # XXX: Could be completely declarative. # Restore application settings. - types = { - 'filename': QString, - 'recentFiles': QStringList, - 'window/size': QSize, - 'window/position': QPoint, - 'window/geometry': QByteArray, - # Docks and toolbars: - 'window/state': QByteArray, - 'savedir': QString, - 'lastOpenDir': QString, - } + + if have_qstring(): + types = { + 'filename': QString, + 'recentFiles': QStringList, + 'window/size': QSize, + 'window/position': QPoint, + 'window/geometry': QByteArray, + 'line/color': QColor, + 'fill/color': QColor, + 'advanced': bool, + # Docks and toolbars: + 'window/state': QByteArray, + 'savedir': QString, + 'lastOpenDir': QString, + } + else: + types = { + 'filename': str, + 'recentFiles': list, + 'window/size': QSize, + 'window/position': QPoint, + 'window/geometry': QByteArray, + 'line/color': QColor, + 'fill/color': QColor, + 'advanced': bool, + # Docks and toolbars: + 'window/state': QByteArray, + 'savedir': str, + 'lastOpenDir': str, + } + self.settings = settings = Settings(types) - self.recentFiles = list(settings['recentFiles']) + self.recentFiles = list(settings.get('recentFiles', [])) size = settings.get('window/size', QSize(600, 500)) position = settings.get('window/position', QPoint(0, 0)) self.resize(size) self.move(position) saveDir = settings.get('savedir', None) self.lastOpenDir = settings.get('lastOpenDir', None) - if os.path.exists(unicode(saveDir)): - self.defaultSaveDir = unicode(saveDir) + if os.path.exists(str(saveDir)): + self.defaultSaveDir = str(saveDir) self.statusBar().showMessage('%s started. Annotation will be saved to %s' %(__appname__, self.defaultSaveDir)) self.statusBar().show() # or simply: #self.restoreGeometry(settings['window/geometry'] - self.restoreState(settings['window/state']) + self.restoreState(settings.get('window/state', QByteArray())) self.lineColor = QColor(settings.get('line/color', Shape.line_color)) self.fillColor = QColor(settings.get('fill/color', Shape.fill_color)) Shape.line_color = self.lineColor Shape.fill_color = self.fillColor - if settings.get('advanced', QVariant()).toBool(): + def xbool(x): + if isinstance(x, QVariant): + return x.toBool() + return bool(x) + + if xbool(settings.get('advanced', False)): self.actions.advancedMode.setChecked(True) self.toggleAdvancedMode() @@ -472,7 +535,7 @@ class MainWindow(QMainWindow, WindowMixin): self.actions.editMode.setEnabled(not drawing) if not drawing and self.beginner(): # Cancel creation. - print 'Cancel creation.' + print('Cancel creation.') self.canvas.setEditing(True) self.canvas.restoreCursor() self.actions.create.setEnabled(True) @@ -493,7 +556,7 @@ class MainWindow(QMainWindow, WindowMixin): def updateFileMenu(self): current = self.filename def exists(filename): - return os.path.exists(unicode(filename)) + return os.path.exists(filename) menu = self.menus.recentFiles menu.clear() files = [f for f in self.recentFiles if f != current and exists(f)] @@ -518,7 +581,7 @@ class MainWindow(QMainWindow, WindowMixin): # Tzutalin 20160906 : Add file list and dock to move faster def fileitemDoubleClicked(self, item=None): - currIndex = self.mImgList.index(str(item.text())) + currIndex = self.mImgList.index(item.text()) if currIndex < len(self.mImgList): filename = self.mImgList[currIndex] if filename: @@ -531,7 +594,7 @@ class MainWindow(QMainWindow, WindowMixin): else: shape = self.canvas.selectedShape if shape: - self.labelList.setItemSelected(self.shapesToItems[shape], True) + self.shapesToItems[shape].setSelected(True) else: self.labelList.clearSelection() self.actions.delete.setEnabled(selected) @@ -541,7 +604,7 @@ class MainWindow(QMainWindow, WindowMixin): self.actions.shapeFillColor.setEnabled(selected) def addLabel(self, shape): - item = QListWidgetItem(shape.label) + item = HashableQListWidgetItem(shape.label) item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) self.itemsToShapes[item] = shape @@ -574,7 +637,7 @@ class MainWindow(QMainWindow, WindowMixin): def saveLabels(self, filename): lf = LabelFile() def format_shape(s): - return dict(label=unicode(s.label), + return dict(label=s.label, line_color=s.line_color.getRgb()\ if s.line_color != self.lineColor else None, fill_color=s.fill_color.getRgb()\ @@ -585,16 +648,16 @@ class MainWindow(QMainWindow, WindowMixin): # Can add differrent annotation formats here try: if self.usingPascalVocFormat is True: - print 'savePascalVocFormat save to:' + filename - lf.savePascalVocFormat(filename, shapes, unicode(self.filename), self.imageData, + print('savePascalVocFormat save to:' + filename) + lf.savePascalVocFormat(filename, shapes, str(self.filename), self.imageData, self.lineColor.getRgb(), self.fillColor.getRgb()) else: - lf.save(filename, shapes, unicode(self.filename), self.imageData, + lf.save(filename, shapes, str(self.filename), self.imageData, self.lineColor.getRgb(), self.fillColor.getRgb()) self.labelFile = lf self.filename = filename return True - except LabelFileError, e: + except LabelFileError as e: self.errorMessage(u'Error saving label data', u'%s' % e) return False @@ -612,9 +675,9 @@ class MainWindow(QMainWindow, WindowMixin): def labelItemChanged(self, item): shape = self.itemsToShapes[item] - label = unicode(item.text()) + label = item.text() if label != shape.label: - shape.label = unicode(item.text()) + shape.label = item.text() self.setDirty() else: # User probably changed item visibility self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked) @@ -677,7 +740,7 @@ class MainWindow(QMainWindow, WindowMixin): self.adjustScale() def togglePolygons(self, value): - for item, shape in self.itemsToShapes.iteritems(): + for item, shape in self.itemsToShapes.items(): item.setCheckState(Qt.Checked if value else Qt.Unchecked) def loadFile(self, filename=None): @@ -685,21 +748,20 @@ class MainWindow(QMainWindow, WindowMixin): self.resetState() self.canvas.setEnabled(False) if filename is None: - filename = self.settings['filename'] - filename = unicode(filename) + filename = self.settings.get('filename') # Tzutalin 20160906 : Add file list and dock to move faster # Highlight the file item if filename and self.fileListWidget.count() > 0: index = self.mImgList.index(filename) fileWidgetItem = self.fileListWidget.item(index) - self.fileListWidget.setItemSelected(fileWidgetItem, True) + fileWidgetItem.setSelected(True) - if QFile.exists(filename): + if filename and QFile.exists(filename): if LabelFile.isLabelFile(filename): try: self.labelFile = LabelFile(filename) - except LabelFileError, e: + except LabelFileError as e: self.errorMessage(u'Error opening file', (u"
%s
" u"Make sure %s is a valid label file.")\ @@ -720,7 +782,7 @@ class MainWindow(QMainWindow, WindowMixin): u"
Make sure %s is a valid image file." % filename) self.status("Error reading %s" % filename) return False - self.status("Loaded %s" % os.path.basename(unicode(filename))) + self.status("Loaded %s" % os.path.basename(str(filename))) self.image = image self.filename = filename self.canvas.loadPixmap(QPixmap.fromImage(image)) @@ -782,7 +844,7 @@ class MainWindow(QMainWindow, WindowMixin): s = self.settings # If it loads images from dir, don't load it at the begining if self.dirname is None: - s['filename'] = self.filename if self.filename else QString() + s['filename'] = self.filename if self.filename else '' else: s['filename'] = '' @@ -820,17 +882,18 @@ class MainWindow(QMainWindow, WindowMixin): for file in files: if file.lower().endswith(tuple(extensions)): relatviePath = os.path.join(root, file) - images.append(os.path.abspath(relatviePath)) + path = u(os.path.abspath(relatviePath)) + images.append(path) images.sort(key=lambda x: x.lower()) return images def changeSavedir(self, _value=False): if self.defaultSaveDir is not None: - path = unicode(self.defaultSaveDir) + path = str(self.defaultSaveDir) else: path = '.' - dirpath = unicode(QFileDialog.getExistingDirectory(self, + dirpath = str(QFileDialog.getExistingDirectory(self, '%s - Save to the directory' % __appname__, path, QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)) @@ -844,14 +907,14 @@ class MainWindow(QMainWindow, WindowMixin): if self.filename is None: return - path = os.path.dirname(unicode(self.filename))\ + path = os.path.dirname(str(self.filename))\ if self.filename else '.' if self.usingPascalVocFormat: - formats = ['*.%s' % unicode(fmt).lower()\ + formats = ['*.%s' % str(fmt).lower()\ for fmt in QImageReader.supportedImageFormats()] filters = "Open Annotation XML file (%s)" % \ ' '.join(formats + ['*.xml']) - filename = unicode(QFileDialog.getOpenFileName(self, + filename = str(QFileDialog.getOpenFileName(self, '%s - Choose a xml file' % __appname__, path, filters)) self.loadPascalXMLByFilename(filename) @@ -859,13 +922,13 @@ class MainWindow(QMainWindow, WindowMixin): if not self.mayContinue(): return - path = os.path.dirname(unicode(self.filename))\ + path = os.path.dirname(self.filename)\ if self.filename else '.' if self.lastOpenDir is not None and len(self.lastOpenDir) > 1: path = self.lastOpenDir - dirpath = unicode(QFileDialog.getExistingDirectory(self, + dirpath = str(QFileDialog.getExistingDirectory(self, '%s - Open Directory' % __appname__, path, QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)) @@ -921,13 +984,13 @@ class MainWindow(QMainWindow, WindowMixin): def openFile(self, _value=False): if not self.mayContinue(): return - path = os.path.dirname(unicode(self.filename))\ + path = os.path.dirname(str(self.filename))\ if self.filename else '.' - formats = ['*.%s' % unicode(fmt).lower()\ + formats = ['*.%s' % str(fmt).lower()\ for fmt in QImageReader.supportedImageFormats()] filters = "Image & Label files (%s)" % \ ' '.join(formats + ['*%s' % LabelFile.suffix]) - filename = unicode(QFileDialog.getOpenFileName(self, + filename = str(QFileDialog.getOpenFileName(self, '%s - Choose Image or Label file' % __appname__, path, filters)) if filename: self.loadFile(filename) @@ -936,7 +999,7 @@ class MainWindow(QMainWindow, WindowMixin): assert not self.image.isNull(), "cannot save empty image" if self.hasLabels(): if self.defaultSaveDir is not None and len(str(self.defaultSaveDir)): - print 'handle the image:' + self.filename + print('handle the image:' + self.filename) imgFileName = os.path.basename(self.filename) savedFileName = os.path.splitext(imgFileName)[0] + LabelFile.suffix savedPath = os.path.join(str(self.defaultSaveDir), savedFileName) @@ -957,7 +1020,6 @@ class MainWindow(QMainWindow, WindowMixin): dlg = QFileDialog(self, caption, openDialogPath, filters) dlg.setDefaultSuffix(LabelFile.suffix[1:]) dlg.setAcceptMode(QFileDialog.AcceptSave) - dlg.setConfirmOverwrite(True) filenameWithoutExtension = os.path.splitext(self.filename)[0] dlg.selectFile(filenameWithoutExtension) dlg.setOption(QFileDialog.DontUseNativeDialog, False) @@ -1002,7 +1064,7 @@ class MainWindow(QMainWindow, WindowMixin): '
%s
%s' % (title, message)) def currentPath(self): - return os.path.dirname(unicode(self.filename)) if self.filename else '.' + return os.path.dirname(str(self.filename)) if self.filename else '.' def chooseColor1(self): color = self.colorDialog.getColor(self.lineColor, u'Choose line color', @@ -1098,10 +1160,20 @@ class Settings(object): def _cast(self, key, value): # XXX: Very nasty way of converting types to QVariant methods :P - t = self.types[key] - if t != QVariant: - method = getattr(QVariant, re.sub('^Q', 'to', t.__name__, count=1)) - return method(value) + print('_cast', key, repr(value)) + t = self.types.get(key) + print('t', t) + if t is not None and t != QVariant: + if t is str: + return str(value) + else: + # XXX: awful + try: + method = getattr(QVariant, re.sub('^Q', 'to', t.__name__, count=1)) + return method(value) + except AttributeError as e: + print(e) + return value return value @@ -1115,13 +1187,21 @@ def read(filename, default=None): except: return default -def main(argv): - """Standard boilerplate Qt application code.""" +def get_main_app(argv=[]): + """ + Standard boilerplate Qt application code. + Do everything but app.exec_() -- so that we can test the application in one thread + """ app = QApplication(argv) app.setApplicationName(__appname__) app.setWindowIcon(newIcon("app")) win = MainWindow(argv[1] if len(argv) == 2 else None) win.show() + return app, win + +def main(argv): + '''construct main app and run it''' + app, _win = get_main_app(argv) return app.exec_() if __name__ == '__main__': diff --git a/libs/canvas.py b/libs/canvas.py index be211e23..b7e4aafd 100644 --- a/libs/canvas.py +++ b/libs/canvas.py @@ -1,5 +1,12 @@ -from PyQt4.QtGui import * -from PyQt4.QtCore import * + +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + #from PyQt4.QtOpenGL import * from shape import Shape @@ -20,7 +27,7 @@ class Canvas(QWidget): shapeMoved = pyqtSignal() drawingPolygon = pyqtSignal(bool) - CREATE, EDIT = range(2) + CREATE, EDIT = list(range(2)) epsilon = 11.0 @@ -85,7 +92,7 @@ class Canvas(QWidget): def mouseMoveEvent(self, ev): """Update line with last point and current coordinates.""" - pos = self.transformPos(ev.posF()) + pos = self.transformPos(ev.pos()) self.restoreCursor() @@ -169,7 +176,8 @@ class Canvas(QWidget): self.hVertex, self.hShape = None, None def mousePressEvent(self, ev): - pos = self.transformPos(ev.posF()) + pos = self.transformPos(ev.pos()) + if ev.button() == Qt.LeftButton: if self.drawing(): if self.current and self.current.reachMaxPoints() is False: @@ -456,12 +464,14 @@ class Canvas(QWidget): return QPointF(min(max(0, x2), max(x3, x4)), y3) return QPointF(x, y) - def intersectingEdges(self, (x1, y1), (x2, y2), points): + def intersectingEdges(self, x1y1, x2y2, points): """For each edge formed by `points', yield the intersection with the line segment `(x1,y1) - (x2,y2)`, if it exists. Also return the distance of `(x2,y2)' to the middle of the edge along with its index, so that the one closest can be chosen.""" - for i in xrange(4): + x1, y1 = x1y1 + x2, y2 = x2y2 + for i in range(4): x3, y3 = points[i] x4, y4 = points[(i+1) % 4] denom = (y4-y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) @@ -506,7 +516,7 @@ class Canvas(QWidget): def keyPressEvent(self, ev): key = ev.key() if key == Qt.Key_Escape and self.current: - print 'ESC press' + print('ESC press') self.current = None self.drawingPolygon.emit(False) self.update() diff --git a/libs/colorDialog.py b/libs/colorDialog.py index 3e712f8a..8c0164a9 100644 --- a/libs/colorDialog.py +++ b/libs/colorDialog.py @@ -1,5 +1,10 @@ -from PyQt4.QtGui import * -from PyQt4.QtCore import * +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import QColorDialog, QDialogButtonBox +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * BB = QDialogButtonBox diff --git a/libs/labelDialog.py b/libs/labelDialog.py index 7f2b8992..c7f70575 100644 --- a/libs/labelDialog.py +++ b/libs/labelDialog.py @@ -1,5 +1,10 @@ -from PyQt4.QtGui import * -from PyQt4.QtCore import * +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * from lib import newIcon, labelValidator @@ -32,11 +37,20 @@ class LabelDialog(QDialog): self.setLayout(layout) def validate(self): - if self.edit.text().trimmed(): - self.accept() + try: + if self.edit.text().trimmed(): + self.accept() + except AttributeError: + # PyQt5: AttributeError: 'str' object has no attribute 'trimmed' + if self.edit.text().strip(): + self.accept() def postProcess(self): - self.edit.setText(self.edit.text().trimmed()) + try: + self.edit.setText(self.edit.text().trimmed()) + except AttributeError: + # PyQt5: AttributeError: 'str' object has no attribute 'trimmed' + self.edit.setText(self.edit.text()) def popUp(self, text='', move=True): self.edit.setText(text) @@ -47,6 +61,10 @@ class LabelDialog(QDialog): return self.edit.text() if self.exec_() else None def listItemClick(self, tQListWidgetItem): - text = tQListWidgetItem.text().trimmed() + try: + text = tQListWidgetItem.text().trimmed() + except AttributeError: + # PyQt5: AttributeError: 'str' object has no attribute 'trimmed' + text = tQListWidgetItem.text().strip() self.edit.setText(text) self.validate() diff --git a/libs/labelFile.py b/libs/labelFile.py index 2665d7f7..f4e490ce 100644 --- a/libs/labelFile.py +++ b/libs/labelFile.py @@ -1,7 +1,11 @@ # Copyright (c) 2016 Tzutalin # Create by TzuTaLin