class _FileWatcher(QObject): """File watcher. QFileSystemWatcher notifies client about any change (file access mode, modification date, etc.) But, we need signal, only after file contents had been changed """ modified = pyqtSignal(bool) removed = pyqtSignal(bool) def __init__(self, path): QObject.__init__(self) self._contents = None self._watcher = QFileSystemWatcher() self._timer = None self._path = path self._lastEmittedModifiedStatus = None self._lastEmittedRemovedStatus = None self.setPath(path) self.enable() def __del__(self): self._stopTimer() def enable(self): """Enable signals from the watcher """ self._watcher.fileChanged.connect(self._onFileChanged) def disable(self): """Disable signals from the watcher """ self._watcher.fileChanged.disconnect(self._onFileChanged) self._stopTimer() def setContents(self, contents): """Set file contents. Watcher uses it to compare old and new contents of the file. """ self._contents = contents # Qt File watcher may work incorrectly, if file was not existing, when it started if not self._watcher.files(): self.setPath(self._path) self._lastEmittedModifiedStatus = None self._lastEmittedRemovedStatus = None def setPath(self, path): """Path had been changed or file had been created. Set new path """ if self._watcher.files(): self._watcher.removePaths(self._watcher.files()) if path is not None and os.path.isfile(path): self._watcher.addPath(path) self._path = path self._lastEmittedModifiedStatus = None self._lastEmittedRemovedStatus = None def _emitModifiedStatus(self): """Emit self.modified signal with right status """ isModified = self._contents != self._safeRead(self._path) if isModified != self._lastEmittedModifiedStatus: self.modified.emit(isModified) self._lastEmittedModifiedStatus = isModified def _emitRemovedStatus(self, isRemoved): """Emit 'removed', if status changed""" if isRemoved != self._lastEmittedRemovedStatus: self._lastEmittedRemovedStatus = isRemoved self.removed.emit(isRemoved) def _onFileChanged(self): """File changed. Emit own signal, if contents changed """ if os.path.exists(self._path): self._emitModifiedStatus() else: self._emitRemovedStatus(True) # Sometimes QFileSystemWatcher emits only 1 signal for 2 modifications # Check once more later self._startTimer() def _startTimer(self): """Init a timer. It is used for monitoring file after deletion. Git removes file, than restores it. """ if self._timer is None: self._timer = QTimer() self._timer.setInterval(500) self._timer.timeout.connect(self._onCheckIfDeletedTimer) self._timer.start() def _stopTimer(self): """Stop timer, if exists """ if self._timer is not None: self._timer.stop() def _onCheckIfDeletedTimer(self): """Check, if file has been restored """ if os.path.exists(self._path): self.setPath(self._path) # restart Qt file watcher after file has been restored self._stopTimer() self._emitRemovedStatus(False) self._emitModifiedStatus() def _safeRead(self, path): """Read file. Ignore exceptions """ try: with open(path, 'rb') as file: return file.read() except (OSError, IOError): return None
class ProjectWidget(Ui_Form, QWidget): SampleWidgetRole = Qt.UserRole + 1 projectsaved = pyqtSignal() projectupdated = pyqtSignal(object) projectloaded = pyqtSignal(object) selectlayersupdated = pyqtSignal(list) def __init__(self, parent=None): super(ProjectWidget, self).__init__(parent) self.setupUi(self) self.project = None self.bar = None self.roamapp = None menu = QMenu() # self.roamVersionLabel.setText("You are running IntraMaps Roam version {}".format(roam.__version__)) self.openProjectFolderButton.pressed.connect(self.openprojectfolder) self.openinQGISButton.pressed.connect(self.openinqgis) self.depolyProjectButton.pressed.connect(self.deploy_project) self.depolyInstallProjectButton.pressed.connect( functools.partial(self.deploy_project, True)) self.filewatcher = QFileSystemWatcher() self.filewatcher.fileChanged.connect(self.qgisprojectupdated) self.projectupdatedlabel.linkActivated.connect(self.reloadproject) self.projectupdatedlabel.hide() # self.setpage(4) self.currentnode = None self.form = None qgislocation = r'C:\OSGeo4W\bin\qgis.bat' qgislocation = roam.config.settings.setdefault('configmanager', {}) \ .setdefault('qgislocation', qgislocation) self.qgispathEdit.setText(qgislocation) self.qgispathEdit.textChanged.connect(self.save_qgis_path) self.filePickerButton.pressed.connect(self.set_qgis_path) self.connect_page_events() def connect_page_events(self): """ Connect the events from all the pages back to here """ for index in range(self.stackedWidget.count()): widget = self.stackedWidget.widget(index) if hasattr(widget, "raiseMessage"): widget.raiseMessage.connect(self.bar.pushMessage) def set_qgis_path(self): """ Set the location of the QGIS install. We need the path to be able to create Roam projects """ path = QFileDialog.getOpenFileName(self, "Select QGIS install file", filter="(*.bat)") if not path: return self.qgispathEdit.setText(path) self.save_qgis_path(path) def save_qgis_path(self, path): """ Save the QGIS path back to the Roam config. """ roam.config.settings['configmanager'] = {'qgislocation': path} roam.config.save() def setpage(self, page, node): """ Set the current page in the config manager. We pass the project into the current page so that it knows what the project is. """ self.currentnode = node self.write_config_currentwidget() self.stackedWidget.setCurrentIndex(page) widget = self.stackedWidget.currentWidget() if hasattr(widget, "set_project"): widget.set_project(self.project, self.currentnode) def write_config_currentwidget(self): """ Call the write config command on the current widget. """ widget = self.stackedWidget.currentWidget() if hasattr(widget, "write_config"): widget.write_config() def deploy_project(self, with_data=False): """ Run the step to deploy a project. Projects are deplyed as a bundled zip of the project folder. """ if self.roamapp.sourcerun: base = os.path.join(self.roamapp.apppath, "..") else: base = self.roamapp.apppath default = os.path.join(base, "roam_serv") path = roam.config.settings.get("publish", {}).get("path", '') if not path: path = default path = os.path.join(path, "projects") if not os.path.exists(path): os.makedirs(path) self._saveproject() options = {} bundle.bundle_project(self.project, path, options, as_install=with_data) def setaboutinfo(self): """ Set the current about info on the widget """ self.versionLabel.setText(roam.__version__) self.qgisapiLabel.setText(unicode(QGis.QGIS_VERSION)) def selectlayerschanged(self, *args): """ Run the updates when the selection layers have changed """ self.formlayers.setSelectLayers(self.project.selectlayers) self.selectlayersupdated.emit(self.project.selectlayers) def reloadproject(self, *args): """ Reload the project. At the moment this will drop any unsaved changes to the config. Note: Should look at making sure it doesn't do that because it's not really needed. """ self.projectupdated.emit(self.project) # self.setproject(self.project) def qgisprojectupdated(self, path): """ Show a message when the QGIS project file has been updated. """ self.projectupdatedlabel.show() self.projectupdatedlabel.setText( "The QGIS project has been updated. <a href='reload'> " "Click to reload</a>. <b style=\"color:red\">Unsaved data will be lost</b>" ) def openinqgis(self): """ Open a QGIS session for the user to config the project layers. """ try: openqgis(self.project.projectfile) except OSError: self.bar.pushMessage("Looks like I couldn't find QGIS", "Check qgislocation in roam.config", QgsMessageBar.WARNING) def openprojectfolder(self): """ Open the project folder in the file manager for the OS. """ folder = self.project.folder openfolder(folder) def setproject(self, project, loadqgis=True): """ Set the widgets active project. """ self.filewatcher.removePaths(self.filewatcher.files()) self.projectupdatedlabel.hide() self._closeqgisproject() if project.valid: self.startsettings = copy.deepcopy(project.settings) self.project = project self.projectlabel.setText(project.name) self.loadqgisproject(project, self.project.projectfile) self.filewatcher.addPath(self.project.projectfile) self.projectloaded.emit(self.project) def loadqgisproject(self, project, projectfile): print("Load QGIS Project!!") QDir.setCurrent(os.path.dirname(project.projectfile)) fileinfo = QFileInfo(project.projectfile) # No idea why we have to set this each time. Maybe QGIS deletes it for # some reason. self.badLayerHandler = BadLayerHandler(callback=self.missing_layers) QgsProject.instance().setBadLayerHandler(self.badLayerHandler) QgsProject.instance().read(fileinfo) def missing_layers(self, missinglayers): """ Handle any and show any missing layers. """ self.project.missing_layers = missinglayers def _closeqgisproject(self): """ Close the current QGIS project and clean up after.. """ QGIS.close_project() def _saveproject(self): """ Save the project config to disk. """ self.write_config_currentwidget() # self.project.dump_settings() self.project.save(update_version=True) self.filewatcher.removePaths(self.filewatcher.files()) QgsProject.instance().write() self.filewatcher.addPath(self.project.projectfile) self.projectsaved.emit()
class ProjectWidget(Ui_Form, QWidget): SampleWidgetRole = Qt.UserRole + 1 projectsaved = pyqtSignal() projectupdated = pyqtSignal() projectloaded = pyqtSignal(object) selectlayersupdated = pyqtSignal(list) def __init__(self, parent=None): super(ProjectWidget, self).__init__(parent) self.setupUi(self) self.project = None self.mapisloaded = False self.bar = None self.roamapp = None menu = QMenu() self.canvas.setCanvasColor(Qt.white) self.canvas.enableAntiAliasing(True) self.canvas.setWheelAction(QgsMapCanvas.WheelZoomToMouseCursor) self.canvas.mapRenderer().setLabelingEngine(QgsPalLabeling()) # self.roamVersionLabel.setText("You are running IntraMaps Roam version {}".format(roam.__version__)) self.openProjectFolderButton.pressed.connect(self.openprojectfolder) self.openinQGISButton.pressed.connect(self.openinqgis) self.depolyProjectButton.pressed.connect(self.deploy_project) self.depolyInstallProjectButton.pressed.connect( functools.partial(self.deploy_project, True)) self.filewatcher = QFileSystemWatcher() self.filewatcher.fileChanged.connect(self.qgisprojectupdated) self.projectupdatedlabel.linkActivated.connect(self.reloadproject) self.projectupdatedlabel.hide() # self.setpage(4) self.currentnode = None self.form = None qgislocation = r'C:\OSGeo4W\bin\qgis.bat' qgislocation = roam.config.settings.setdefault('configmanager', {}) \ .setdefault('qgislocation', qgislocation) self.qgispathEdit.setText(qgislocation) self.qgispathEdit.textChanged.connect(self.save_qgis_path) self.filePickerButton.pressed.connect(self.set_qgis_path) def set_qgis_path(self): path = QFileDialog.getOpenFileName(self, "Select QGIS install file", filter="(*.bat)") if not path: return self.qgispathEdit.setText(path) self.save_qgis_path(path) def save_qgis_path(self, path): roam.config.settings['configmanager'] = {'qgislocation': path} roam.config.save() def setpage(self, page, node): self.currentnode = node self.write_config_currentwidget() self.stackedWidget.setCurrentIndex(page) if self.project: print self.project.dump_settings() widget = self.stackedWidget.currentWidget() if hasattr(widget, "set_project"): widget.set_project(self.project, self.currentnode) def write_config_currentwidget(self): widget = self.stackedWidget.currentWidget() if hasattr(widget, "write_config"): widget.write_config() def deploy_project(self, with_data=False): if self.roamapp.sourcerun: base = os.path.join(self.roamapp.apppath, "..") else: base = self.roamapp.apppath default = os.path.join(base, "roam_serv") path = roam.config.settings.get("publish", {}).get("path", '') if not path: path = default path = os.path.join(path, "projects") if not os.path.exists(path): os.makedirs(path) self._saveproject() options = {} bundle.bundle_project(self.project, path, options, as_install=with_data) def setaboutinfo(self): self.versionLabel.setText(roam.__version__) self.qgisapiLabel.setText(str(QGis.QGIS_VERSION)) def checkcapturelayers(self): haslayers = self.project.hascapturelayers() self.formslayerlabel.setVisible(not haslayers) return haslayers def selectlayerschanged(self, *args): self.formlayers.setSelectLayers(self.project.selectlayers) self.checkcapturelayers() self.selectlayersupdated.emit(self.project.selectlayers) def reloadproject(self, *args): self.setproject(self.project) self.projectupdated.emit() def qgisprojectupdated(self, path): self.projectupdatedlabel.show() self.projectupdatedlabel.setText( "The QGIS project has been updated. <a href='reload'> " "Click to reload</a>. <b style=\"color:red\">Unsaved data will be lost</b>" ) def openinqgis(self): projectfile = self.project.projectfile qgislocation = r'C:\OSGeo4W\bin\qgis.bat' qgislocation = roam.config.settings.setdefault('configmanager', {}) \ .setdefault('qgislocation', qgislocation) try: openqgis(projectfile, qgislocation) except OSError: self.bar.pushMessage("Looks like I couldn't find QGIS", "Check qgislocation in roam.config", QgsMessageBar.WARNING) def openprojectfolder(self): folder = self.project.folder openfolder(folder) def setproject(self, project, loadqgis=True): """ Set the widgets active project. """ self.mapisloaded = False self.filewatcher.removePaths(self.filewatcher.files()) self.projectupdatedlabel.hide() self._closeqgisproject() if project.valid: self.startsettings = copy.deepcopy(project.settings) self.project = project self.projectlabel.setText(project.name) self.loadqgisproject(project, self.project.projectfile) self.filewatcher.addPath(self.project.projectfile) self.projectloaded.emit(self.project) def loadqgisproject(self, project, projectfile): QDir.setCurrent(os.path.dirname(project.projectfile)) fileinfo = QFileInfo(project.projectfile) self.projectLocationLabel.setText("Project File: {}".format( os.path.basename(project.projectfile))) QgsProject.instance().read(fileinfo) def _closeqgisproject(self): if self.canvas.isDrawing(): return self.canvas.freeze(True) QgsMapLayerRegistry.instance().removeAllMapLayers() self.canvas.freeze(False) def loadmap(self): if self.mapisloaded: return # This is a dirty hack to work around the timer that is in QgsMapCanvas in 2.2. # Refresh will stop the canvas timer # Repaint will redraw the widget. # loadmap is only called once per project load so it's safe to do this here. self.canvas.refresh() self.canvas.repaint() parser = roam.projectparser.ProjectParser.fromFile( self.project.projectfile) canvasnode = parser.canvasnode self.canvas.mapRenderer().readXML(canvasnode) self.canvaslayers = parser.canvaslayers() self.canvas.setLayerSet(self.canvaslayers) self.canvas.updateScale() self.canvas.refresh() self.mapisloaded = True def _saveproject(self): """ Save the project config to disk. """ self.write_config_currentwidget() # self.project.dump_settings() self.project.save(update_version=True) self.projectsaved.emit()
class ProjectWidget(Ui_Form, QWidget): SampleWidgetRole = Qt.UserRole + 1 projectsaved = pyqtSignal() projectupdated = pyqtSignal() projectloaded = pyqtSignal(object) selectlayersupdated = pyqtSignal(list) projectlocationchanged = pyqtSignal(str) def __init__(self, parent=None): super(ProjectWidget, self).__init__(parent) self.setupUi(self) self.project = None self.mapisloaded = False self.bar = None self.canvas.setCanvasColor(Qt.white) self.canvas.enableAntiAliasing(True) self.canvas.setWheelAction(QgsMapCanvas.WheelZoomToMouseCursor) self.canvas.mapRenderer().setLabelingEngine(QgsPalLabeling()) self.fieldsmodel = QgsFieldModel() self.widgetmodel = WidgetsModel() self.possiblewidgetsmodel = QStandardItemModel() self.formlayersmodel = QgsLayerModel(watchregistry=False) self.formlayers = CaptureLayerFilter() self.formlayers.setSourceModel(self.formlayersmodel) self.selectlayermodel = CaptureLayersModel(watchregistry=False) self.selectlayerfilter = LayerTypeFilter() self.selectlayerfilter.setSourceModel(self.selectlayermodel) self.selectlayermodel.dataChanged.connect(self.selectlayerschanged) self.layerCombo.setModel(self.formlayers) self.widgetCombo.setModel(self.possiblewidgetsmodel) self.selectLayers.setModel(self.selectlayerfilter) self.selectLayers_2.setModel(self.selectlayerfilter) self.fieldList.setModel(self.fieldsmodel) self.widgetlist.setModel(self.widgetmodel) self.widgetlist.selectionModel().currentChanged.connect(self.updatecurrentwidget) self.widgetmodel.rowsRemoved.connect(self.setwidgetconfigvisiable) self.widgetmodel.rowsInserted.connect(self.setwidgetconfigvisiable) self.widgetmodel.modelReset.connect(self.setwidgetconfigvisiable) self.titleText.textChanged.connect(self.updatetitle) QgsProject.instance().readProject.connect(self._readproject) self.loadwidgettypes() self.addWidgetButton.pressed.connect(self.newwidget) self.removeWidgetButton.pressed.connect(self.removewidget) self.roamVersionLabel.setText("You are running IntraMaps Roam version {}".format(roam.__version__)) self.openProjectFolderButton.pressed.connect(self.openprojectfolder) self.openinQGISButton.pressed.connect(self.openinqgis) self.filewatcher = QFileSystemWatcher() self.filewatcher.fileChanged.connect(self.qgisprojectupdated) self.formfolderLabel.linkActivated.connect(self.openformfolder) self.projectupdatedlabel.linkActivated.connect(self.reloadproject) self.projectupdatedlabel.hide() self.formtab.currentChanged.connect(self.formtabchanged) self.expressionButton.clicked.connect(self.opendefaultexpression) self.fieldList.currentIndexChanged.connect(self.updatewidgetname) self.fieldwarninglabel.hide() for item, data in readonlyvalues: self.readonlyCombo.addItem(item, data) self.setpage(4) self.form = None self.projectlocations.currentIndexChanged[str].connect(self.projectlocationchanged.emit) def setaboutinfo(self): self.versionLabel.setText(roam.__version__) self.qgisapiLabel.setText(str(QGis.QGIS_VERSION)) def checkcapturelayers(self): haslayers = self.project.hascapturelayers() self.formslayerlabel.setVisible(not haslayers) return haslayers def opendefaultexpression(self): layer = self.currentform.QGISLayer dlg = QgsExpressionBuilderDialog(layer, "Create default value expression", self) text = self.defaultvalueText.text().strip('[%').strip('%]').strip() dlg.setExpressionText(text) if dlg.exec_(): self.defaultvalueText.setText('[% {} %]'.format(dlg.expressionText())) def openformfolder(self, url): openfolder(url) def selectlayerschanged(self, *args): self.formlayers.setSelectLayers(self.project.selectlayers) self.checkcapturelayers() self.selectlayersupdated.emit(self.project.selectlayers) def formtabchanged(self, index): # preview if index == 1: self.form.settings['widgets'] = list(self.widgetmodel.widgets()) self.setformpreview(self.form) def setprojectfolders(self, folders): for folder in folders: self.projectlocations.addItem(folder) def setpage(self, page): self.stackedWidget.setCurrentIndex(page) def reloadproject(self, *args): self.setproject(self.project) def qgisprojectupdated(self, path): self.projectupdatedlabel.show() self.projectupdatedlabel.setText("The QGIS project has been updated. <a href='reload'> " "Click to reload</a>. <b style=\"color:red\">Unsaved data will be lost</b>") def openinqgis(self): projectfile = self.project.projectfile qgislocation = r'C:\OSGeo4W\bin\qgis.bat' qgislocation = roam.config.settings.setdefault('configmanager', {}) \ .setdefault('qgislocation', qgislocation) try: openqgis(projectfile, qgislocation) except WindowsError: self.bar.pushMessage("Looks like I couldn't find QGIS", "Check qgislocation in settings.config", QgsMessageBar.WARNING) def openprojectfolder(self): folder = self.project.folder openfolder(folder) def setwidgetconfigvisiable(self, *args): haswidgets = self.widgetmodel.rowCount() > 0 self.widgetframe.setEnabled(haswidgets) def removewidget(self): """ Remove the selected widget from the widgets list """ widget, index = self.currentuserwidget if index.isValid(): self.widgetmodel.removeRow(index.row(), index.parent()) def newwidget(self): """ Create a new widget. The default is a list. """ widget = {} widget['widget'] = 'List' # Grab the first field. widget['field'] = self.fieldsmodel.index(0, 0).data(QgsFieldModel.FieldNameRole) currentindex = self.widgetlist.currentIndex() currentitem = self.widgetmodel.itemFromIndex(currentindex) if currentitem and currentitem.iscontainor(): parent = currentindex else: parent = currentindex.parent() index = self.widgetmodel.addwidget(widget, parent) self.widgetlist.setCurrentIndex(index) def loadwidgettypes(self): self.widgetCombo.blockSignals(True) for widgettype in roam.editorwidgets.core.supportedwidgets(): try: configclass = configmanager.editorwidgets.widgetconfigs[widgettype] except KeyError: continue configwidget = configclass() item = QStandardItem(widgettype) item.setData(configwidget, Qt.UserRole) item.setData(widgettype, Qt.UserRole + 1) item.setIcon(QIcon(widgeticon(widgettype))) self.widgetCombo.model().appendRow(item) self.widgetstack.addWidget(configwidget) self.widgetCombo.blockSignals(False) def usedfields(self): """ Return the list of fields that have been used by the the current form's widgets """ for widget in self.currentform.widgets: yield widget['field'] @property def currentform(self): """ Return the current selected form. """ return self.form @property def currentuserwidget(self): """ Return the selected user widget. """ index = self.widgetlist.currentIndex() return index.data(Qt.UserRole), index @property def currentwidgetconfig(self): """ Return the selected widget in the widget combo. """ index = self.widgetCombo.currentIndex() index = self.possiblewidgetsmodel.index(index, 0) return index.data(Qt.UserRole), index, index.data(Qt.UserRole + 1) def updatewidgetname(self, index): # Only change the edit text on name field if it's not already set to something other then the # field name. field = self.fieldsmodel.index(index, 0).data(QgsFieldModel.FieldNameRole) currenttext = self.nameText.text() foundfield = self.fieldsmodel.findfield(currenttext) if foundfield: self.nameText.setText(field) def _save_widgetfield(self, index): """ Save the selected field for the current widget. Shows a error if the field is already used but will allow the user to still set it in the case of extra logic for that field in the forms Python logic. """ widget, index = self.currentuserwidget row = self.fieldList.currentIndex() field = self.fieldsmodel.index(row, 0).data(QgsFieldModel.FieldNameRole) showwarning = field in self.usedfields() self.fieldwarninglabel.setVisible(showwarning) widget['field'] = field self.widgetmodel.setData(index, widget, Qt.UserRole) def _save_selectedwidget(self, index): configwidget, index, widgettype = self.currentwidgetconfig widget, index = self.currentuserwidget if not widget: return widget['widget'] = widgettype widget['required'] = self.requiredCheck.isChecked() widget['config'] = configwidget.getconfig() widget['name'] = self.nameText.text() widget['read-only-rules'] = [self.readonlyCombo.itemData(self.readonlyCombo.currentIndex())] widget['hidden'] = self.hiddenCheck.isChecked() self.widgetmodel.setData(index, widget, Qt.UserRole) def _save_default(self): widget, index = self.currentuserwidget default = self.defaultvalueText.text() widget['default'] = default self.widgetmodel.setData(index, widget, Qt.UserRole) def _save_selectionlayers(self, index, layer, value): config = self.project.settings self.selectlayermodel.dataChanged.emit(index, index) def _save_formtype(self, index): formtype = self.formtypeCombo.currentText() form = self.currentform form.settings['type'] = formtype def _save_formname(self, text): """ Save the form label to the settings file. """ try: form = self.currentform if form is None: return form.settings['label'] = text self.projectupdated.emit() except IndexError: return def _save_layer(self, index): """ Save the selected layer to the settings file. """ index = self.formlayers.index(index, 0) layer = index.data(Qt.UserRole) if not layer: return form = self.currentform if form is None: return form.settings['layer'] = layer.name() self.updatefields(layer) def setsplash(self, splash): pixmap = QPixmap(splash) w = self.splashlabel.width() h = self.splashlabel.height() self.splashlabel.setPixmap(pixmap.scaled(w,h, Qt.KeepAspectRatio)) def setproject(self, project, loadqgis=True): """ Set the widgets active project. """ self.disconnectsignals() self.mapisloaded = False self.filewatcher.removePaths(self.filewatcher.files()) self.projectupdatedlabel.hide() self._closeqgisproject() if project.valid: self.startsettings = copy.deepcopy(project.settings) self.project = project self.projectlabel.setText(project.name) self.versionText.setText(project.version) self.selectlayermodel.config = project.settings self.formlayers.setSelectLayers(self.project.selectlayers) self.setsplash(project.splash) self.loadqgisproject(project, self.project.projectfile) self.filewatcher.addPath(self.project.projectfile) self.projectloaded.emit(self.project) def loadqgisproject(self, project, projectfile): QDir.setCurrent(os.path.dirname(project.projectfile)) fileinfo = QFileInfo(project.projectfile) QgsProject.instance().read(fileinfo) def _closeqgisproject(self): if self.canvas.isDrawing(): return self.canvas.freeze(True) self.formlayersmodel.removeall() self.selectlayermodel.removeall() QgsMapLayerRegistry.instance().removeAllMapLayers() self.canvas.freeze(False) def loadmap(self): if self.mapisloaded: return # This is a dirty hack to work around the timer that is in QgsMapCanvas in 2.2. # Refresh will stop the canvas timer # Repaint will redraw the widget. # loadmap is only called once per project load so it's safe to do this here. self.canvas.refresh() self.canvas.repaint() parser = roam.projectparser.ProjectParser.fromFile(self.project.projectfile) canvasnode = parser.canvasnode self.canvas.mapRenderer().readXML(canvasnode) self.canvaslayers = parser.canvaslayers() self.canvas.setLayerSet(self.canvaslayers) self.canvas.updateScale() self.canvas.refresh() self.mapisloaded = True def _readproject(self, doc): self.formlayersmodel.refresh() self.selectlayermodel.refresh() self._updateforproject(self.project) def _updateforproject(self, project): self.titleText.setText(project.name) self.descriptionText.setPlainText(project.description) def swapwidgetconfig(self, index): widgetconfig, _, _ = self.currentwidgetconfig defaultvalue = widgetconfig.defaultvalue self.defaultvalueText.setText(defaultvalue) self.updatewidgetconfig({}) def updatetitle(self, text): self.project.settings['title'] = text self.projectlabel.setText(text) self.projectupdated.emit() def updatewidgetconfig(self, config): widgetconfig, index, widgettype = self.currentwidgetconfig self.setconfigwidget(widgetconfig, config) def setformpreview(self, form): def removewidget(): item = self.frame_2.layout().itemAt(0) if item and item.widget(): item.widget().setParent(None) removewidget() featureform = FeatureForm.from_form(form, form.settings, None, {}) self.frame_2.layout().addWidget(featureform) def connectsignals(self): self.formLabelText.textChanged.connect(self._save_formname) self.layerCombo.currentIndexChanged.connect(self._save_layer) self.formtypeCombo.currentIndexChanged.connect(self._save_formtype) #widget settings self.fieldList.currentIndexChanged.connect(self._save_widgetfield) self.requiredCheck.toggled.connect(self._save_selectedwidget) self.defaultvalueText.textChanged.connect(self._save_default) self.widgetCombo.currentIndexChanged.connect(self._save_selectedwidget) self.widgetCombo.currentIndexChanged.connect(self.swapwidgetconfig) self.nameText.textChanged.connect(self._save_selectedwidget) self.readonlyCombo.currentIndexChanged.connect(self._save_selectedwidget) self.hiddenCheck.toggled.connect(self._save_selectedwidget) def disconnectsignals(self): try: self.formLabelText.textChanged.disconnect(self._save_formname) self.layerCombo.currentIndexChanged.disconnect(self._save_layer) self.formtypeCombo.currentIndexChanged.disconnect(self._save_formtype) #widget settings self.fieldList.currentIndexChanged.disconnect(self._save_widgetfield) self.requiredCheck.toggled.disconnect(self._save_selectedwidget) self.defaultvalueText.textChanged.disconnect(self._save_default) self.widgetCombo.currentIndexChanged.disconnect(self._save_selectedwidget) self.widgetCombo.currentIndexChanged.disconnect(self.swapwidgetconfig) self.nameText.textChanged.disconnect(self._save_selectedwidget) self.readonlyCombo.currentIndexChanged.disconnect(self._save_selectedwidget) self.hiddenCheck.toggled.disconnect(self._save_selectedwidget) except TypeError: pass def setform(self, form): """ Update the UI with the currently selected form. """ def getfirstlayer(): index = self.formlayers.index(0,0) layer = index.data(Qt.UserRole) layer = layer.name() return layer def loadwidgets(widget): """ Load the widgets into widgets model """ self.widgetmodel.clear() self.widgetmodel.loadwidgets(form.widgets) def findlayer(layername): index = self.formlayersmodel.findlayer(layername) index = self.formlayers.mapFromSource(index) layer = index.data(Qt.UserRole) return index, layer self.disconnectsignals() self.form = form settings = form.settings label = form.label layername = settings.setdefault('layer', getfirstlayer()) layerindex, layer = findlayer(layername) if not layer or not layerindex.isValid(): return formtype = settings.setdefault('type', 'auto') widgets = settings.setdefault('widgets', []) self.formLabelText.setText(label) folderurl = "<a href='{path}'>{name}</a>".format(path=form.folder, name=os.path.basename(form.folder)) self.formfolderLabel.setText(folderurl) self.layerCombo.setCurrentIndex(layerindex.row()) self.updatefields(layer) index = self.formtypeCombo.findText(formtype) if index == -1: self.formtypeCombo.insertItem(0, formtype) self.formtypeCombo.setCurrentIndex(0) else: self.formtypeCombo.setCurrentIndex(index) loadwidgets(widgets) # Set the first widget index = self.widgetmodel.index(0, 0) if index.isValid(): self.widgetlist.setCurrentIndex(index) self.updatecurrentwidget(index, None) self.connectsignals() def updatefields(self, layer): """ Update the UI with the fields for the selected layer. """ self.fieldsmodel.setLayer(layer) def setconfigwidget(self, configwidget, config): """ Set the active config widget. """ try: configwidget.widgetdirty.disconnect(self._save_selectedwidget) except TypeError: pass #self.descriptionLabel.setText(configwidget.description) self.widgetstack.setCurrentWidget(configwidget) configwidget.setconfig(config) configwidget.widgetdirty.connect(self._save_selectedwidget) def updatecurrentwidget(self, index, _): """ Update the UI with the config for the current selected widget. """ if not index.isValid(): return widget = index.data(Qt.UserRole) widgettype = widget['widget'] field = widget['field'] required = widget.setdefault('required', False) name = widget.setdefault('name', field) default = widget.setdefault('default', '') readonly = widget.setdefault('read-only-rules', []) hidden = widget.setdefault('hidden', False) try: data = readonly[0] except: data = 'never' self.readonlyCombo.blockSignals(True) index = self.readonlyCombo.findData(data) self.readonlyCombo.setCurrentIndex(index) self.readonlyCombo.blockSignals(False) self.defaultvalueText.blockSignals(True) if not isinstance(default, dict): self.defaultvalueText.setText(default) else: # TODO Handle the more advanced default values. pass self.defaultvalueText.blockSignals(False) self.nameText.blockSignals(True) self.nameText.setText(name) self.nameText.blockSignals(False) self.requiredCheck.blockSignals(True) self.requiredCheck.setChecked(required) self.requiredCheck.blockSignals(False) self.hiddenCheck.blockSignals(True) self.hiddenCheck.setChecked(hidden) self.hiddenCheck.blockSignals(False) if not field is None: self.fieldList.blockSignals(True) index = self.fieldList.findData(field.lower(), QgsFieldModel.FieldNameRole) if index > -1: self.fieldList.setCurrentIndex(index) else: self.fieldList.setEditText(field) self.fieldList.blockSignals(False) index = self.widgetCombo.findText(widgettype) self.widgetCombo.blockSignals(True) if index > -1: self.widgetCombo.setCurrentIndex(index) self.widgetCombo.blockSignals(False) self.updatewidgetconfig(config=widget.setdefault('config', {})) def _saveproject(self): """ Save the project config to disk. """ title = self.titleText.text() description = self.descriptionText.toPlainText() version = str(self.versionText.text()) settings = self.project.settings settings['title'] = title settings['description'] = description settings['version'] = version form = self.currentform if form: form.settings['widgets'] = list(self.widgetmodel.widgets()) logger.debug(form.settings) self.project.save() self.projectsaved.emit()
class MainWindow(QMainWindow): workingDirectory = '' fileNames = [] supportedExtensions = QStringList(('*.txt','*.csv')) def __init__(self): QMainWindow.__init__(self) self.settings = QSettings("greyltc", "batch-iv-analysis") self.rows = 0 #keep track of how many rows there are in the table self.cols = OrderedDict() thisKey = 'plotBtn' self.cols[thisKey] = col() self.cols[thisKey].header = 'Draw Plot' self.cols[thisKey].tooltip = 'Click this button to draw a plot for that row' thisKey = 'exportBtn' self.cols[thisKey] = col() self.cols[thisKey].header = 'Export' self.cols[thisKey].tooltip = 'Click this button to export\ninterpolated data points from fits' thisKey = 'file' self.cols[thisKey] = col() self.cols[thisKey].header = 'File' self.cols[thisKey].tooltip = 'File name\nHover to see header from data file' thisKey = 'pce' self.cols[thisKey] = col() self.cols[thisKey].header = 'PCE\n[%]' self.cols[thisKey].tooltip = 'Power conversion efficiency as found from spline fit\nHover for value from characteristic equation fit' thisKey = 'pmax' self.cols[thisKey] = col() self.cols[thisKey].header = 'P_max\n[mW/cm^2]' self.cols[thisKey].tooltip = 'Maximum power density as found from spline fit\nHover for value from characteristic equation fit' thisKey = 'jsc' self.cols[thisKey] = col() self.cols[thisKey].header = 'J_sc\n[mA/cm^2]' self.cols[thisKey].tooltip = 'Short-circuit current density as found from spline fit\nHover for value from characteristic equation fit' thisKey = 'voc' self.cols[thisKey] = col() self.cols[thisKey].header = 'V_oc\n[mV]' self.cols[thisKey].tooltip = 'Open-circuit voltage as found from spline fit\nHover for value from characteristic equation fit' thisKey = 'ff' self.cols[thisKey] = col() self.cols[thisKey].header = 'FF' self.cols[thisKey].tooltip = 'Fill factor as found from spline fit\nHover for value from characteristic equation fit' thisKey = 'rs' self.cols[thisKey] = col() self.cols[thisKey].header = 'R_s\n[ohm*cm^2]' self.cols[thisKey].tooltip = 'Specific series resistance as found from characteristic equation fit\nHover for 95% confidence interval' thisKey = 'rsh' self.cols[thisKey] = col() self.cols[thisKey].header = 'R_sh\n[ohm*cm^2]' self.cols[thisKey].tooltip = 'Specific shunt resistance as found from characteristic equation fit\nHover for 95% confidence interval' thisKey = 'jph' self.cols[thisKey] = col() self.cols[thisKey].header = 'J_ph\n[mA/cm^2]' self.cols[thisKey].tooltip = 'Photogenerated current density as found from characteristic equation fit\nHover for 95% confidence interval' thisKey = 'j0' self.cols[thisKey] = col() self.cols[thisKey].header = 'J_0\n[nA/cm^2]' self.cols[thisKey].tooltip = 'Reverse saturation current density as found from characteristic equation fit\nHover for 95% confidence interval' thisKey = 'n' self.cols[thisKey] = col() self.cols[thisKey].header = 'n' self.cols[thisKey].tooltip = 'Diode ideality factor as found from characteristic equation fit\nHover for 95% confidence interval' thisKey = 'Vmax' self.cols[thisKey] = col() self.cols[thisKey].header = 'V_max\n[mV]' self.cols[thisKey].tooltip = 'Voltage at maximum power point as found from spline fit\nHover for value from characteristic equation fit' thisKey = 'area' self.cols[thisKey] = col() self.cols[thisKey].header = 'Area\n[cm^2]' self.cols[thisKey].tooltip = 'Device area' thisKey = 'pmax2' self.cols[thisKey] = col() self.cols[thisKey].header = 'P_max\n[mW]' self.cols[thisKey].tooltip = 'Maximum power as found from spline fit\nHover for value from characteristic equation fit' thisKey = 'isc' self.cols[thisKey] = col() self.cols[thisKey].header = 'I_sc\n[mA]' self.cols[thisKey].tooltip = 'Short-circuit current as found from characteristic equation fit\nHover for 95% confidence interval' thisKey = 'iph' self.cols[thisKey] = col() self.cols[thisKey].header = 'I_ph\n[mA]' self.cols[thisKey].tooltip = 'Photogenerated current as found from characteristic equation fit\nHover for 95% confidence interval' thisKey = 'i0' self.cols[thisKey] = col() self.cols[thisKey].header = 'I_0\n[nA]' self.cols[thisKey].tooltip = 'Reverse saturation current as found from characteristic equation fit\nHover for 95% confidence interval' thisKey = 'rs2' self.cols[thisKey] = col() self.cols[thisKey].header = 'R_s\n[ohm]' self.cols[thisKey].tooltip = 'Series resistance as found from characteristic equation fit\nHover for 95% confidence interval' thisKey = 'rsh2' self.cols[thisKey] = col() self.cols[thisKey].header = 'R_sh\n[ohm]' self.cols[thisKey].tooltip = 'Shunt resistance as found from characteristic equation fit\nHover for 95% confidence interval' #how long status messages show for self.messageDuration = 2500#ms # Set up the user interface from Designer. self.ui = Ui_batch_iv_analysis() self.ui.setupUi(self) #insert cols for item in self.cols: blankItem = QTableWidgetItem() thisCol = self.cols.keys().index(item) self.ui.tableWidget.insertColumn(thisCol) blankItem.setToolTip(self.cols[item].tooltip) blankItem.setText(self.cols[item].header) self.ui.tableWidget.setHorizontalHeaderItem(thisCol,blankItem) #file system watcher self.watcher = QFileSystemWatcher(self) self.watcher.directoryChanged.connect(self.handleWatchUpdate) #connect signals generated by gui elements to proper functions self.ui.actionOpen.triggered.connect(self.openCall) self.ui.actionEnable_Watching.triggered.connect(self.watchCall) self.ui.actionSave.triggered.connect(self.handleSave) self.ui.actionWatch_2.triggered.connect(self.handleWatchAction) self.ui.actionClear_Table.triggered.connect(self.clearTableCall) def exportInterp(self,row): thisGraphData = self.ui.tableWidget.item(row,self.cols.keys().index('plotBtn')).data(Qt.UserRole).toPyObject() fitX = thisGraphData[QString(u'fitX')] modelY = thisGraphData[QString(u'modelY')] splineY = thisGraphData[QString(u'splineY')] a = np.asarray([fitX, modelY, splineY]) a = np.transpose(a) destinationFolder = os.path.join(self.workingDirectory,'exports') QDestinationFolder = QDir(destinationFolder) if not QDestinationFolder.exists(): QDir().mkdir(destinationFolder) saveFile = os.path.join(destinationFolder,str(self.ui.tableWidget.item(row,self.cols.keys().index('file')).text())+'.csv') header = 'Voltage [V],CharEqn Current [mA/cm^2],Spline Current [mA/cm^2]' try: np.savetxt(saveFile, a, delimiter=",",header=header) self.ui.statusbar.showMessage("Exported " + saveFile,5000) except: self.ui.statusbar.showMessage("Could not export " + saveFile,self.messageDuration) def handleButton(self): btn = self.sender() #kinda hacky: row = self.ui.tableWidget.indexAt(btn.pos()).row() col = self.ui.tableWidget.indexAt(btn.pos()).column() if col == 0: self.rowGraph(row) if col == 1: self.exportInterp(row) def rowGraph(self,row): thisGraphData = self.ui.tableWidget.item(row,self.cols.keys().index('plotBtn')).data(Qt.UserRole).toPyObject() filename = str(self.ui.tableWidget.item(row,self.cols.keys().index('file')).text()) v = thisGraphData[QString(u'v')] i = thisGraphData[QString(u'i')] if not thisGraphData[QString(u'vsTime')]: plt.plot(v, i, c='b', marker='o', ls="None",label='I-V Data') plt.scatter(thisGraphData[QString(u'Vmax')], thisGraphData[QString(u'Imax')], c='g',marker='x',s=100) plt.scatter(thisGraphData[QString(u'Voc')], 0, c='g',marker='x',s=100) plt.scatter(0, thisGraphData[QString(u'Isc')], c='g',marker='x',s=100) fitX = thisGraphData[QString(u'fitX')] modelY = thisGraphData[QString(u'modelY')] splineY = thisGraphData[QString(u'splineY')] if not np.isnan(modelY[0]): plt.plot(fitX, modelY,c='k', label='CharEqn Best Fit') plt.plot(fitX, splineY,c='g', label='Spline Fit') plt.autoscale(axis='x', tight=True) plt.grid(b=True) ax = plt.gca() handles, labels = ax.get_legend_handles_labels() ax.legend(handles, labels, loc=3) plt.annotate( thisGraphData[QString(u'Voc')].__format__('0.4f')+ ' V', xy = (thisGraphData[QString(u'Voc')], 0), xytext = (40, 20), textcoords = 'offset points', ha = 'right', va = 'bottom', bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5), arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0')) plt.annotate( float(thisGraphData[QString(u'Isc')]).__format__('0.4f') + ' mA/cm^2', xy = (0,thisGraphData[QString(u'Isc')]), xytext = (40, 20), textcoords = 'offset points', ha = 'right', va = 'bottom', bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5), arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0')) plt.annotate( float(thisGraphData[QString(u'Imax')]*thisGraphData[QString(u'Vmax')]).__format__('0.4f') + '% @(' + float(thisGraphData[QString(u'Vmax')]).__format__('0.4f') + ',' + float(thisGraphData[QString(u'Imax')]).__format__('0.4f') + ')', xy = (thisGraphData[QString(u'Vmax')],thisGraphData[QString(u'Imax')]), xytext = (80, 40), textcoords = 'offset points', ha = 'right', va = 'bottom', bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5), arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0')) plt.ylabel('Current [mA/cm^2]') plt.xlabel('Voltage [V]') else: #vs time time = thisGraphData[QString(u'time')] fig, ax1 = plt.subplots() ax1.plot(time, v, 'b-',label='Voltage [V]') ax1.set_xlabel('Time [s]') # Make the y-axis label and tick labels match the line color. ax1.set_ylabel('Voltage [V]', color='b') for tl in ax1.get_yticklabels(): tl.set_color('b') #fdsf ax2 = ax1.twinx() ax2.plot(time, i, 'r-') ax2.set_ylabel('Current [mA/cm^2]', color='r') for tl in ax2.get_yticklabels(): tl.set_color('r') plt.title(filename) plt.draw() plt.show() def handleSave(self): if self.settings.contains('lastFolder'): saveDir = self.settings.value('lastFolder').toString() else: saveDir = '.' path = QFileDialog.getSaveFileName(self, caption='Save File', directory=saveDir) if not str(path[0]) == '': with open(unicode(path), 'wb') as stream: writer = csv.writer(stream) rowdata = [] for column in range(self.ui.tableWidget.columnCount()): item = self.ui.tableWidget.horizontalHeaderItem(column) if item is not None: rowdata.append(unicode(item.text()).encode('utf8').replace('\n',' ')) else: rowdata.append('') writer.writerow(rowdata) for row in range(self.ui.tableWidget.rowCount()): rowdata = [] for column in range(self.ui.tableWidget.columnCount()): item = self.ui.tableWidget.item(row, column) if item is not None: rowdata.append(unicode(item.text()).encode('utf8')) else: rowdata.append('') writer.writerow(rowdata) stream.close() def clearTableCall(self): for ii in range(self.rows): self.ui.tableWidget.removeRow(0) self.ui.tableWidget.clearContents() self.rows = 0 self.fileNames = [] def processFile(self,fullPath): fileName, fileExtension = os.path.splitext(fullPath) fileName = os.path.basename(fullPath) self.fileNames.append(fileName) if fileExtension == '.csv': delimiter = ',' else: delimiter = None self.ui.statusbar.showMessage("processing: "+ fileName,2500) #wait here for the file to be completely written to disk and closed before trying to read it fi = QFileInfo(fullPath) while (not fi.isWritable()): time.sleep(0.001) fi.refresh() fp = open(fullPath, mode='r') fileBuffer = fp.read() fp.close() first10 = fileBuffer[0:10] nMcHeaderLines = 25 #number of header lines in mcgehee IV file format isMcFile = False #true if this is a McGehee iv file format if (not first10.__contains__('#')) and (first10.__contains__('/')) and (first10.__contains__('\t')):#the first line is not a comment #the first 8 chars do not contain comment symbol and do contain / and a tab, it's safe to assume mcgehee iv file format isMcFile = True #comment out the first 25 rows here fileBuffer = '#'+fileBuffer fileBuffer = fileBuffer.replace('\n', '\n#',nMcHeaderLines-1) splitBuffer = fileBuffer.splitlines(True) area = 1 noArea = True vsTime = False #this is not an i,v vs t data file #extract header lines and search for area header = [] for line in splitBuffer: if line.startswith('#'): header.append(line) if line.__contains__('Area'): area = float(line.split(' ')[3]) noArea = False if line.__contains__('I&V vs t'): if float(line.split(' ')[5]) == 1: vsTime = True else: break outputScaleFactor = np.array(1000/area) #for converstion to [mA/cm^2] tempFile = QTemporaryFile() tempFile.open() tempFile.writeData(fileBuffer) tempFile.flush() #read in data try: data = np.loadtxt(str(tempFile.fileName()),delimiter=delimiter) VV = data[:,0] II = data[:,1] if vsTime: time = data[:,2] except: self.ui.statusbar.showMessage('Could not read' + fileName +'. Prepend # to all non-data lines and try again',2500) return tempFile.close() tempFile.remove() if isMcFile: #convert to amps II = II/1000*area if not vsTime: #sort data by ascending voltage newOrder = VV.argsort() VV=VV[newOrder] II=II[newOrder] #remove duplicate voltage entries VV, indices = np.unique(VV, return_index =True) II = II[indices] else: #sort data by ascending time newOrder = time.argsort() VV=VV[newOrder] II=II[newOrder] time=time[newOrder] time=time-time[0]#start time at t=0 #catch and fix flipped current sign: if II[0] < II[-1]: II = II * -1 indexInQuad1 = np.logical_and(VV>0,II>0) if any(indexInQuad1): #enters statement if there is at least one datapoint in quadrant 1 isDarkCurve = False else: self.ui.statusbar.showMessage("Dark curve detected",500) isDarkCurve = True #put items in table self.ui.tableWidget.insertRow(self.rows) for ii in range(len(self.cols)): self.ui.tableWidget.setItem(self.rows,ii,QTableWidgetItem()) if not vsTime: fitParams, fitCovariance, infodict, errmsg, ier = self.bestEffortFit(VV,II) #print errmsg I0_fit = fitParams[0] Iph_fit = fitParams[1] Rs_fit = fitParams[2] Rsh_fit = fitParams[3] n_fit = fitParams[4] #0 -> LS-straight line #1 -> cubic spline interpolant smoothingParameter = 1-2e-6 iFitSpline = SmoothSpline(VV, II, p=smoothingParameter) def cellModel(voltageIn): #voltageIn = np.array(voltageIn) return vectorizedCurrent(voltageIn, I0_fit, Iph_fit, Rs_fit, Rsh_fit, n_fit) def invCellPowerSpline(voltageIn): if voltageIn < 0: return 0 else: return -1*voltageIn*iFitSpline(voltageIn) def invCellPowerModel(voltageIn): if voltageIn < 0: return 0 else: return -1*voltageIn*cellModel(voltageIn) if not isDarkCurve: VVq1 = VV[indexInQuad1] IIq1 = II[indexInQuad1] vMaxGuess = VVq1[np.array(VVq1*IIq1).argmax()] powerSearchResults = optimize.minimize(invCellPowerSpline,vMaxGuess) #catch a failed max power search: if not powerSearchResults.status == 0: print "power search exit code = " + str(powerSearchResults.status) print powerSearchResults.message vMax = nan iMax = nan pMax = nan else: vMax = powerSearchResults.x[0] iMax = iFitSpline([vMax])[0] pMax = vMax*iMax #only do this stuff if the char eqn fit was good if ier < 5: powerSearchResults_charEqn = optimize.minimize(invCellPowerModel,vMaxGuess) #catch a failed max power search: if not powerSearchResults_charEqn.status == 0: print "power search exit code = " + str(powerSearchResults_charEqn.status) print powerSearchResults_charEqn.message vMax_charEqn = nan else: vMax_charEqn = powerSearchResults_charEqn.x[0] #dude try: Voc_nn_charEqn=optimize.brentq(cellModel, VV[0], VV[-1]) except: Voc_nn_charEqn = nan else: Voc_nn_charEqn = nan vMax_charEqn = nan try: Voc_nn = optimize.brentq(iFitSpline, VV[0], VV[-1]) except: Voc_nn = nan else: Voc_nn = nan vMax = nan iMax = nan pMax = nan Voc_nn_charEqn = nan vMax_charEqn = nan iMax_charEqn = nan pMax_charEqn = nan if ier < 5: dontFindBounds = False iMax_charEqn = cellModel([vMax_charEqn])[0] pMax_charEqn = vMax_charEqn*iMax_charEqn Isc_nn_charEqn = cellModel(0) FF_charEqn = pMax_charEqn/(Voc_nn_charEqn*Isc_nn_charEqn) else: dontFindBounds = True iMax_charEqn = nan pMax_charEqn = nan Isc_nn_charEqn = nan FF_charEqn = nan #there is a maddening bug in SmoothingSpline: it can't evaluate 0 alone, so I have to do this: try: Isc_nn = iFitSpline([0,1e-55])[0] except: Isc_nn = nan FF = pMax/(Voc_nn*Isc_nn) if (ier != 7) and (ier != 6) and (not dontFindBounds) and (type(fitCovariance) is not float): #error estimation: alpha = 0.05 # 95% confidence interval = 100*(1-alpha) nn = len(VV) # number of data points p = len(fitParams) # number of parameters dof = max(0, nn - p) # number of degrees of freedom # student-t value for the dof and confidence level tval = t.ppf(1.0-alpha/2., dof) lowers = [] uppers = [] #calculate 95% confidence interval for a, p,var in zip(range(nn), fitParams, np.diag(fitCovariance)): sigma = var**0.5 lower = p - sigma*tval upper = p + sigma*tval lowers.append(lower) uppers.append(upper) else: uppers = [nan,nan,nan,nan,nan] lowers = [nan,nan,nan,nan,nan] plotPoints = 1000 fitX = np.linspace(VV[0],VV[-1],plotPoints) if ier < 5: modelY = cellModel(fitX)*outputScaleFactor else: modelY = np.empty(plotPoints)*nan splineY = iFitSpline(fitX)*outputScaleFactor graphData = {'vsTime':vsTime,'origRow':self.rows,'fitX':fitX,'modelY':modelY,'splineY':splineY,'i':II*outputScaleFactor,'v':VV,'Voc':Voc_nn,'Isc':Isc_nn*outputScaleFactor,'Vmax':vMax,'Imax':iMax*outputScaleFactor} #export button exportBtn = QPushButton(self.ui.tableWidget) exportBtn.setText('Export') exportBtn.clicked.connect(self.handleButton) self.ui.tableWidget.setCellWidget(self.rows,self.cols.keys().index('exportBtn'), exportBtn) self.ui.tableWidget.item(self.rows,self.cols.keys().index('pce')).setData(Qt.DisplayRole,round(pMax/area*1e3,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('pce')).setToolTip(str(round(pMax_charEqn/area*1e3,3))) self.ui.tableWidget.item(self.rows,self.cols.keys().index('pmax')).setData(Qt.DisplayRole,round(pMax/area*1e3,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('pmax')).setToolTip(str(round(pMax_charEqn/area*1e3,3))) self.ui.tableWidget.item(self.rows,self.cols.keys().index('jsc')).setData(Qt.DisplayRole,round(Isc_nn/area*1e3,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('jsc')).setToolTip(str(round(Isc_nn_charEqn/area*1e3,3))) self.ui.tableWidget.item(self.rows,self.cols.keys().index('voc')).setData(Qt.DisplayRole,round(Voc_nn*1e3,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('voc')).setToolTip(str(round(Voc_nn_charEqn*1e3,3))) self.ui.tableWidget.item(self.rows,self.cols.keys().index('ff')).setData(Qt.DisplayRole,round(FF,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('ff')).setToolTip(str(round(FF_charEqn,3))) self.ui.tableWidget.item(self.rows,self.cols.keys().index('rs')).setData(Qt.DisplayRole,round(Rs_fit*area,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('rs')).setToolTip('[{0} {1}]'.format(lowers[2]*area, uppers[2]*area)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('rsh')).setData(Qt.DisplayRole,round(Rsh_fit*area,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('rsh')).setToolTip('[{0} {1}]'.format(lowers[3]*area, uppers[3]*area)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('jph')).setData(Qt.DisplayRole,round(Iph_fit/area*1e3,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('jph')).setToolTip('[{0} {1}]'.format(lowers[1]/area*1e3, uppers[1]/area*1e3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('j0')).setData(Qt.DisplayRole,round(I0_fit/area*1e9,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('j0')).setToolTip('[{0} {1}]'.format(lowers[0]/area*1e9, uppers[0]/area*1e9)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('n')).setData(Qt.DisplayRole,round(n_fit,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('n')).setToolTip('[{0} {1}]'.format(lowers[4], uppers[4])) self.ui.tableWidget.item(self.rows,self.cols.keys().index('Vmax')).setData(Qt.DisplayRole,round(vMax*1e3,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('Vmax')).setToolTip(str(round(vMax_charEqn*1e3,3))) self.ui.tableWidget.item(self.rows,self.cols.keys().index('area')).setData(Qt.DisplayRole,round(area,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('pmax2')).setData(Qt.DisplayRole,round(pMax*1e3,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('pmax2')).setToolTip(str(round(pMax_charEqn*1e3,3))) self.ui.tableWidget.item(self.rows,self.cols.keys().index('isc')).setData(Qt.DisplayRole,round(Isc_nn*1e3,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('isc')).setToolTip(str(round(Isc_nn_charEqn*1e3,3))) self.ui.tableWidget.item(self.rows,self.cols.keys().index('iph')).setData(Qt.DisplayRole,round(Iph_fit*1e3,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('iph')).setToolTip('[{0} {1}]'.format(lowers[1]*1e3, uppers[1]*1e3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('i0')).setData(Qt.DisplayRole,round(I0_fit*1e9,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('i0')).setToolTip('[{0} {1}]'.format(lowers[0]*1e9, uppers[0]*1e9)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('rs2')).setData(Qt.DisplayRole,round(Rs_fit,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('rs2')).setToolTip('[{0} {1}]'.format(lowers[2], uppers[2])) self.ui.tableWidget.item(self.rows,self.cols.keys().index('rsh2')).setData(Qt.DisplayRole,round(Rsh_fit,3)) self.ui.tableWidget.item(self.rows,self.cols.keys().index('rsh2')).setToolTip('[{0} {1}]'.format(lowers[3], uppers[3])) else:#vs time graphData = {'vsTime':vsTime,'origRow':self.rows,'time':time,'i':II*outputScaleFactor,'v':VV} #file name self.ui.tableWidget.item(self.rows,self.cols.keys().index('file')).setText(fileName) self.ui.tableWidget.item(self.rows,self.cols.keys().index('file')).setToolTip(''.join(header)) #plot button plotBtn = QPushButton(self.ui.tableWidget) plotBtn.setText('Plot') plotBtn.clicked.connect(self.handleButton) self.ui.tableWidget.setCellWidget(self.rows,self.cols.keys().index('plotBtn'), plotBtn) self.ui.tableWidget.item(self.rows,self.cols.keys().index('plotBtn')).setData(Qt.UserRole,graphData) self.ui.tableWidget.resizeColumnsToContents() self.rows = self.rows + 1 def bestEffortFit(self,VV,II): #splineTestVV=np.linspace(VV[0],VV[-1],1000) #splineTestII=iFitSpline(splineTestVV) #p1, = plt.plot(splineTestVV,splineTestII) #p3, = plt.plot(VV,II,ls='None',marker='o', label='Data') #plt.draw() #plt.show() #data point selection: #lowest voltage (might be same as Isc) V_start_n = VV[0] I_start_n = II[0] #highest voltage V_end_n = VV[-1] I_end_n = II[-1] #Isc iFit = interpolate.interp1d(VV,II) V_sc_n = 0 try: I_sc_n = float(iFit(V_sc_n)) except: return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10]) #mpp VVcalc = VV-VV[0] IIcalc = II-min(II) Pvirtual= np.array(VVcalc*IIcalc) vMaxIndex = Pvirtual.argmax() V_vmpp_n = VV[vMaxIndex] I_vmpp_n = II[vMaxIndex] #Vp: half way in voltage between vMpp and the start of the dataset: V_vp_n = (V_vmpp_n-V_start_n)/2 +V_start_n try: I_vp_n = float(iFit(V_vp_n)) except: return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10]) #Ip: half way in current between vMpp and the end of the dataset: I_ip_n = (I_vmpp_n-I_end_n)/2 + I_end_n iFit2 = interpolate.interp1d(VV,II-I_ip_n) try: V_ip_n = optimize.brentq(iFit2, VV[0], VV[-1]) except: return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10]) diaplayAllGuesses = False def evaluateGuessPlot(dataX, dataY, myguess): myguess = [float(x) for x in myguess] print "myguess:" print myguess vv=np.linspace(min(dataX),max(dataX),1000) ii=vectorizedCurrent(vv,myguess[0],myguess[1],myguess[2],myguess[3],myguess[4]) plt.title('Guess and raw data') plt.plot(vv,ii) plt.scatter(dataX,dataY) plt.grid(b=True) plt.draw() plt.show() #phase 1 guesses: I_L_initial_guess = I_sc_n R_sh_initial_guess = 1e6 #compute intellegent guesses for Iph, Rsh by forcing the curve through several data points and numerically solving the resulting system of eqns newRhs = rhs - I aLine = Rsh*V+Iph-I eqnSys1 = aLine.subs([(V,V_start_n),(I,I_start_n)]) eqnSys2 = aLine.subs([(V,V_vp_n),(I,I_vp_n)]) eqnSys = (eqnSys1,eqnSys2) try: nGuessSln = sympy.nsolve(eqnSys,(Iph,Rsh),(I_L_initial_guess,R_sh_initial_guess),maxsteps=10000) except: return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10]) I_L_guess = nGuessSln[0] R_sh_guess = -1*1/nGuessSln[1] R_s_guess = -1*(V_end_n-V_ip_n)/(I_end_n-I_ip_n) n_initial_guess = 2 #TODO: maybe a more intelegant guess for n can be found using http://pvcdrom.pveducation.org/CHARACT/IDEALITY.HTM I0_initial_guess = eyeNot[0].evalf(subs={Vth:thermalVoltage,Rs:R_s_guess,Rsh:R_sh_guess,Iph:I_L_guess,n:n_initial_guess,I:I_ip_n,V:V_ip_n}) initial_guess = [I0_initial_guess, I_L_guess, R_s_guess, R_sh_guess, n_initial_guess] if diaplayAllGuesses: evaluateGuessPlot(VV, II, initial_guess) # let's try the fit now, if it works great, we're done, otherwise we can continue #try: #guess = initial_guess #fitParams, fitCovariance, infodict, errmsg, ier = optimize.curve_fit(optimizeThis, VV, II,p0=guess,full_output = True,xtol=1e-13,ftol=1e-15) #return(fitParams, fitCovariance, infodict, errmsg, ier) #except: #pass #refine guesses for I0 and Rs by forcing the curve through several data points and numerically solving the resulting system of eqns eqnSys1 = newRhs.subs([(Vth,thermalVoltage),(Iph,I_L_guess),(V,V_ip_n),(I,I_ip_n),(n,n_initial_guess),(Rsh,R_sh_guess)]) eqnSys2 = newRhs.subs([(Vth,thermalVoltage),(Iph,I_L_guess),(V,V_end_n),(I,I_end_n),(n,n_initial_guess),(Rsh,R_sh_guess)]) eqnSys = (eqnSys1,eqnSys2) try: nGuessSln = sympy.nsolve(eqnSys,(I0,Rs),(I0_initial_guess,R_s_guess),maxsteps=10000) except: return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10]) I0_guess = nGuessSln[0] R_s_guess = nGuessSln[1] #Rs_initial_guess = RsEqn[0].evalf(subs={I0:I0_initial_guess,Vth:thermalVoltage,Rsh:R_sh_guess,Iph:I_L_guess,n:n_initial_guess,I:I_end_n,V:V_end_n}) #I0_guess = I0_initial_guess #R_s_guess = Rs_initial_guess guess = [I0_guess, I_L_guess, R_s_guess, R_sh_guess, n_initial_guess] if diaplayAllGuesses: evaluateGuessPlot(VV, II, guess) #nidf #give 5x weight to data around mpp #nP = II*VV #maxIndex = np.argmax(nP) #weights = np.ones(len(II)) #halfRange = (V_ip_n-VV[vMaxIndex])/2 #upperTarget = VV[vMaxIndex] + halfRange #lowerTarget = VV[vMaxIndex] - halfRange #lowerTarget = 0 #upperTarget = V_oc_n #lowerI = np.argmin(abs(VV-lowerTarget)) #upperI = np.argmin(abs(VV-upperTarget)) #weights[range(lowerI,upperI)] = 3 #weights[maxnpi] = 10 #todo: play with setting up "key points" guess = [float(x) for x in guess] #odrMod = odr.Model(odrThing) #myData = odr.Data(VV,II) #myodr = odr.ODR(myData, odrMod, beta0=guess,maxit=5000,sstol=1e-20,partol=1e-20)# #myoutput = myodr.run() #myoutput.pprint() #see http://docs.scipy.org/doc/external/odrpack_guide.pdf try: #myoutput = myodr.run() #fitParams = myoutput.beta #print myoutput.stopreason #print myoutput.info #ier = 1 fitParams, fitCovariance, infodict, errmsg, ier = optimize.curve_fit(optimizeThis, VV, II,p0=guess,full_output = True,xtol=1e-13,ftol=1e-15) #fitParams, fitCovariance, infodict, errmsg, ier = optimize.leastsq(func=residual, args=(VV, II, np.ones(len(II))),x0=guess,full_output=1,xtol=1e-12,ftol=1e-14)#,xtol=1e-12,ftol=1e-14,maxfev=12000 #fitParams, fitCovariance, infodict, errmsg, ier = optimize.leastsq(func=residual, args=(VV, II, weights),x0=fitParams,full_output=1,ftol=1e-15,xtol=0)#,xtol=1e-12,ftol=1e-14 alwaysShowRecap = False if alwaysShowRecap: vv=np.linspace(VV[0],VV[-1],1000) print "fit:" print fitParams print "guess:" print guess print ier print errmsg ii=vectorizedCurrent(vv,guess[0],guess[1],guess[2],guess[3],guess[4]) ii2=vectorizedCurrent(vv,fitParams[0],fitParams[1],fitParams[2],fitParams[3],fitParams[4]) plt.title('Fit analysis') p1, = plt.plot(vv,ii, label='Guess',ls='--') p2, = plt.plot(vv,ii2, label='Fit') p3, = plt.plot(VV,II,ls='None',marker='o', label='Data') #p4, = plt.plot(VV[range(lowerI,upperI)],II[range(lowerI,upperI)],ls="None",marker='o', label='5x Weight Data') ax = plt.gca() handles, labels = ax.get_legend_handles_labels() ax.legend(handles, labels, loc=3) plt.grid(b=True) plt.draw() plt.show() return(fitParams, fitCovariance, infodict, errmsg, ier) except: return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10]) def openCall(self): #remember the last path th user opened if self.settings.contains('lastFolder'): openDir = self.settings.value('lastFolder').toString() else: openDir = '.' fileNames = QFileDialog.getOpenFileNamesAndFilter(directory = openDir, caption="Select one or more files to open", filter = '(*.txt *.csv);;Folders (*)') #fileNames = QFileDialog.getExistingDirectory(directory = openDir, caption="Select one or more files to open") if len(fileNames[0])>0:#check if user clicked cancel self.workingDirectory = os.path.dirname(str(fileNames[0][0])) self.settings.setValue('lastFolder',self.workingDirectory) for fullPath in fileNames[0]: fullPath = str(fullPath) self.processFile(fullPath) if self.ui.actionEnable_Watching.isChecked(): watchedDirs = self.watcher.directories() self.watcher.removePaths(watchedDirs) self.watcher.addPath(self.workingDirectory) self.handleWatchUpdate(self.workingDirectory) #user chose file --> watch def handleWatchAction(self): #remember the last path th user opened if self.settings.contains('lastFolder'): openDir = self.settings.value('lastFolder').toString() else: openDir = '.' myDir = QFileDialog.getExistingDirectory(directory = openDir, caption="Select folder to watch") if len(myDir)>0:#check if user clicked cancel self.workingDirectory = str(myDir) self.settings.setValue('lastFolder',self.workingDirectory) self.ui.actionEnable_Watching.setChecked(True) watchedDirs = self.watcher.directories() self.watcher.removePaths(watchedDirs) self.watcher.addPath(self.workingDirectory) self.handleWatchUpdate(self.workingDirectory) #user toggeled Tools --> Enable Watching def watchCall(self): watchedDirs = self.watcher.directories() self.watcher.removePaths(watchedDirs) if self.ui.actionEnable_Watching.isChecked(): if (self.workingDirectory != ''): self.watcher.addPath(self.workingDirectory) self.handleWatchUpdate(self.workingDirectory) def handleWatchUpdate(self,path): myDir = QDir(path) myDir.setNameFilters(self.supportedExtensions) allFilesNow = myDir.entryList() allFilesNow = list(allFilesNow) allFilesNow = [str(item) for item in allFilesNow] differentFiles = list(set(allFilesNow) ^ set(self.fileNames)) if differentFiles != []: for aFile in differentFiles: if self.fileNames.__contains__(aFile): #TODO: delete the file from the table self.ui.statusbar.showMessage('Removed' + aFile,2500) else: #process the new file self.processFile(os.path.join(self.workingDirectory,aFile))
class Watcher( QObject ): " Filesystem watcher implementation " fsChanged = pyqtSignal( list ) def __init__( self, excludeFilters, dirToWatch ): QObject.__init__( self ) self.__dirWatcher = QFileSystemWatcher( self ) # data members self.__excludeFilter = [] # Files exclude filter self.__srcDirsToWatch = set() # Came from the user self.__fsTopLevelSnapshot = {} # Current snapshot self.__fsSnapshot = {} # Current snapshot # Sets of dirs which are currently watched self.__dirsToWatch = set() self.__topLevelDirsToWatch = set() # Generated till root # precompile filters for flt in excludeFilters: self.__excludeFilter.append( re.compile( flt ) ) # Initialise the list of dirs to watch self.__srcDirsToWatch.add( dirToWatch ) self.__topLevelDirsToWatch = self.__buildTopDirsList( self.__srcDirsToWatch ) self.__fsTopLevelSnapshot = self.__buildTopLevelSnapshot( self.__topLevelDirsToWatch, self.__srcDirsToWatch ) self.__dirsToWatch = self.__buildSnapshot() # Here __dirsToWatch and __topLevelDirsToWatch have a complete # set of what should be watched # Add the dirs to the watcher dirs = [] for path in self.__dirsToWatch | self.__topLevelDirsToWatch: dirs.append( path ) self.__dirWatcher.addPaths( dirs ) self.__dirWatcher.directoryChanged.connect( self.__onDirChanged ) # self.debug() return @staticmethod def __buildTopDirsList( srcDirs ): " Takes a list of dirs to be watched and builds top dirs set " topDirsList = set() for path in srcDirs: parts = path.split( os.path.sep ) for index in xrange( 1, len( parts ) - 1 ): candidate = os.path.sep.join( parts[ 0:index ] ) + os.path.sep if os.path.exists( candidate ): if os.access( candidate, os.R_OK ): topDirsList.add( candidate ) return topDirsList @staticmethod def __buildTopLevelSnapshot( topLevelDirs, srcDirs ): " Takes top level dirs and builds their snapshot " snapshot = {} for path in topLevelDirs: itemsSet = set() # search for all the dirs to be watched for candidate in topLevelDirs | srcDirs: if len( candidate ) <= len( path ): continue if candidate.startswith( path ): candidate = candidate[ len( path ) : ] slashIndex = candidate.find( os.path.sep ) + 1 item = candidate[ : slashIndex ] if os.path.exists( path + item ): itemsSet.add( item ) snapshot[ path ] = itemsSet return snapshot def __buildSnapshot( self ): " Builds the filesystem snapshot " snapshotDirs = set() for path in self.__srcDirsToWatch: self.__addSnapshotPath( path, snapshotDirs ) return snapshotDirs def __addSnapshotPath( self, path, snapshotDirs, itemsToReport = None ): " Adds one path to the FS snapshot " if not os.path.exists( path ): return snapshotDirs.add( path ) dirItems = set() for item in os.listdir( path ): if self.__shouldExclude( item ): continue if os.path.isdir( path + item ): dirName = path + item + os.path.sep dirItems.add( item + os.path.sep ) if itemsToReport is not None: itemsToReport.append( "+" + dirName ) self.__addSnapshotPath( dirName, snapshotDirs, itemsToReport ) continue dirItems.add( item ) if itemsToReport is not None: itemsToReport.append( "+" + path + item ) self.__fsSnapshot[ path ] = dirItems return def __onDirChanged( self, path ): " Triggered when the dir is changed " path = str( path ) if not path.endswith( os.path.sep ): path = path + os.path.sep # Check if it is a top level dir try: oldSet = self.__fsTopLevelSnapshot[ path ] # Build a new set of what is in that top level dir newSet = set() for item in os.listdir( path ): if not os.path.isdir( path + item ): continue # Only dirs are of interest for the top level item = item + os.path.sep if item in oldSet: newSet.add( item ) # Now we have an old set and a new one with those from the old # which actually exist diff = oldSet - newSet # diff are those which disappeared. We need to do the following: # - build a list of all the items in the fs snapshot which start # from this dir # - build a list of dirs which should be deregistered from the # watcher. This list includes both top level and project level # - deregister dirs from the watcher # - emit a signal of what disappeared if not diff: return # no changes self.__fsTopLevelSnapshot[ path ] = newSet dirsToBeRemoved = [] itemsToReport = [] for item in diff: self.__processRemoveTopDir( path + item, dirsToBeRemoved, itemsToReport ) # Here: it is possible that the last dir to watch disappeared if not newSet: # There is nothing to watch here anymore dirsToBeRemoved.append( path ) del self.__fsTopLevelSnapshot[ path ] parts = path[ 1:-1 ].split( os.path.sep ) for index in xrange( len( parts ) - 2, 0, -1 ): candidate = os.path.sep + \ os.path.sep.join( parts[ 0 : index ] ) + \ os.path.sep dirSet = self.__fsTopLevelSnapshot[ candidate ] dirSet.remove( parts[ index + 1 ] + os.path.sep ) if not dirSet: dirsToBeRemoved.append( candidate ) del self.__fsTopLevelSnapshot[ candidate ] continue break # it is not the last item in the set # Update the watcher if dirsToBeRemoved: self.__dirWatcher.removePaths( dirsToBeRemoved ) # Report if itemsToReport: self.fsChanged.emit( itemsToReport ) return except: # it is not a top level dir - no key pass # Here: the change is in the project level dir try: oldSet = self.__fsSnapshot[ path ] # Build a new set of what is in that top level dir newSet = set() for item in os.listdir( path ): if self.__shouldExclude( item ): continue if os.path.isdir( path + item ): newSet.add( item + os.path.sep ) else: newSet.add( item ) # Here: we have a new and old snapshots # Lets calculate the difference deletedItems = oldSet - newSet addedItems = newSet - oldSet if not deletedItems and not addedItems: return # No changes # Update the changed dir set self.__fsSnapshot[ path ] = newSet # We need to build some lists: # - list of files which were added # - list of dirs which were added # - list of files which were deleted # - list of dirs which were deleted # The deleted dirs must be unregistered in the watcher # The added dirs must be registered itemsToReport = [] dirsToBeAdded = [] dirsToBeRemoved = [] for item in addedItems: if item.endswith( os.path.sep ): # directory was added self.__processAddedDir( path + item, dirsToBeAdded, itemsToReport ) else: itemsToReport.append( "+" + path + item ) for item in deletedItems: if item.endswith( os.path.sep ): # directory was deleted self.__processRemovedDir( path + item, dirsToBeRemoved, itemsToReport ) else: itemsToReport.append( "-" + path + item ) # Update the watcher if dirsToBeRemoved: self.__dirWatcher.removePaths( dirsToBeRemoved ) if dirsToBeAdded: self.__dirWatcher.addPaths( dirsToBeAdded ) # Report self.fsChanged.emit( itemsToReport ) except: # It could be a queued signal about what was already reported pass # self.debug() return def __shouldExclude( self, name ): " Tests if a file must be excluded " for excl in self.__excludeFilter: if excl.match( name ): return True return False def __processAddedDir( self, path, dirsToBeAdded, itemsToReport ): " called for an appeared dir in the project tree " dirsToBeAdded.append( path ) itemsToReport.append( "+" + path ) # it should add dirs recursively into the snapshot and care # of the items to report dirItems = set() for item in os.listdir( path ): if self.__shouldExclude( item ): continue if os.path.isdir( path + item ): dirName = path + item + os.path.sep dirItems.add( item + os.path.sep ) self.__processAddedDir( dirName, dirsToBeAdded, itemsToReport ) continue itemsToReport.append( "+" + path + item ) dirItems.add( item ) self.__fsSnapshot[ path ] = dirItems return def __processRemovedDir( self, path, dirsToBeRemoved, itemsToReport ): " called for a disappeared dir in the project tree " # it should remove the dirs recursively from the fs snapshot # and care of items to report dirsToBeRemoved.append( path ) itemsToReport.append( "-" + path ) oldSet = self.__fsSnapshot[ path ] for item in oldSet: if item.endswith( os.path.sep ): # Nested dir self.__processRemovedDir( path + item, dirsToBeRemoved, itemsToReport ) else: # a file itemsToReport.append( "-" + path + item ) del self.__fsSnapshot[ path ] return def __processRemoveTopDir( self, path, dirsToBeRemoved, itemsToReport ): " Called for a disappeared top level dir " if path in self.__fsTopLevelSnapshot: # It is still a top level dir dirsToBeRemoved.append( path ) for item in self.__fsTopLevelSnapshot[ path ]: self.__processRemoveTopDir( path + item, dirsToBeRemoved, itemsToReport ) del self.__fsTopLevelSnapshot[ path ] else: # This is a project level dir self.__processRemovedDir( path, dirsToBeRemoved, itemsToReport ) return def reset( self ): " Resets the watcher (it does not report any changes) " self.__dirWatcher.removePaths( self.__dirWatcher.directories() ) self.__srcDirsToWatch = set() self.__fsTopLevelSnapshot = {} self.__fsSnapshot = {} self.__dirsToWatch = set() self.__topLevelDirsToWatch = set() return def registerDir( self, path ): " Adds a directory to the list of watched ones " if not path.endswith( os.path.sep ): path = path + os.path.sep if path in self.__srcDirsToWatch: return # It is there already # It is necessary to do the following: # - add the dir to the fs snapshot # - collect dirs to add to the watcher # - collect items to report self.__srcDirsToWatch.add( path ) dirsToWatch = set() itemsToReport = [] self.__registerDir( path, dirsToWatch, itemsToReport ) # It might be that top level dirs should be updated too newTopLevelDirsToWatch = self.__buildTopDirsList(self.__srcDirsToWatch) addedDirs = newTopLevelDirsToWatch - self.__topLevelDirsToWatch for item in addedDirs: dirsToWatch.add( item ) # Identify items to be watched by this dir dirItems = set() for candidate in newTopLevelDirsToWatch | self.__srcDirsToWatch: if len( candidate ) <= len( item ): continue if candidate.startswith( item ): candidate = candidate[ len( item ) : ] slashIndex = candidate.find( os.path.sep ) + 1 dirName = candidate[ : slashIndex ] if os.path.exists( item + dirName ): dirItems.add( dirName ) # Update the top level dirs snapshot self.__fsTopLevelSnapshot[ item ] = dirItems # Update the top level snapshot with the added dir upperDir = os.path.dirname( path[ :-1 ] ) + os.path.sep dirName = path.replace( upperDir, '' ) self.__fsTopLevelSnapshot[ upperDir ].add( dirName ) # Update the list of top level dirs to watch self.__topLevelDirsToWatch = newTopLevelDirsToWatch # Update the watcher if dirsToWatch: dirs = [] for item in dirsToWatch: dirs.append( item ) self.__dirWatcher.addPaths( dirs ) # Report the changes if itemsToReport: self.fsChanged.emit( itemsToReport ) # self.debug() return def __registerDir( self, path, dirsToWatch, itemsToReport ): " Adds one path to the FS snapshot " if not os.path.exists( path ): return dirsToWatch.add( path ) itemsToReport.append( "+" + path ) dirItems = set() for item in os.listdir( path ): if self.__shouldExclude( item ): continue if os.path.isdir( path + item ): dirName = path + item + os.path.sep dirItems.add( item + os.path.sep ) itemsToReport.append( "+" + path + item + os.path.sep ) self.__addSnapshotPath( dirName, dirsToWatch, itemsToReport ) continue dirItems.add( item ) itemsToReport.append( "+" + path + item ) self.__fsSnapshot[ path ] = dirItems return def deregisterDir( self, path ): " Removes the directory from the list of the watched ones " if not path.endswith( os.path.sep ): path = path + os.path.sep if path not in self.__srcDirsToWatch: return # It is not there already self.__srcDirsToWatch.remove( path ) # It is necessary to do the following: # - remove the dir from the fs snapshot # - collect the dirs to be removed from watching # - collect item to report itemsToReport = [] dirsToBeRemoved = [] self.__deregisterDir( path, dirsToBeRemoved, itemsToReport ) # It is possible that some of the top level watched dirs should be # removed as well newTopLevelDirsToWatch = self.__buildTopDirsList(self.__srcDirsToWatch) deletedDirs = self.__topLevelDirsToWatch - newTopLevelDirsToWatch for item in deletedDirs: dirsToBeRemoved.append( item ) del self.__fsTopLevelSnapshot[ item ] # It might be the case that some of the items should be deleted in the # top level dirs sets for dirName in self.__fsTopLevelSnapshot: itemsSet = self.__fsTopLevelSnapshot[ dirName ] for item in itemsSet: candidate = dirName + item if candidate == path or candidate in deletedDirs: itemsSet.remove( item ) self.__fsTopLevelSnapshot[ dirName ] = itemsSet break # Update the list of dirs to be watched self.__topLevelDirsToWatch = newTopLevelDirsToWatch # Update the watcher if dirsToBeRemoved: self.__dirWatcher.removePaths( dirsToBeRemoved ) # Report the changes if itemsToReport: self.fsChanged.emit( itemsToReport ) # self.debug() return def __deregisterDir( self, path, dirsToBeRemoved, itemsToReport ): " Deregisters a directory recursively " dirsToBeRemoved.append( path ) itemsToReport.append( "-" + path ) if path in self.__fsTopLevelSnapshot: # This is a top level dir for item in self.__fsTopLevelSnapshot[ path ]: if item.endswith( os.path.sep ): # It's a dir self.__deregisterDir( path + item, dirsToBeRemoved, itemsToReport ) else: # It's a file itemsToReport.append( "-" + path + item ) del self.__fsTopLevelSnapshot[ path ] return # It is from an a project level snapshot if path in self.__fsSnapshot: for item in self.__fsSnapshot[ path ]: if item.endswith( os.path.sep ): # It's a dir self.__deregisterDir( path + item, dirsToBeRemoved, itemsToReport ) else: # It's a file itemsToReport.append( "-" + path + item ) del self.__fsSnapshot[ path ] return def debug( self ): print "Top level dirs to watch: " + str( self.__topLevelDirsToWatch ) print "Project dirs to watch: " + str( self.__dirsToWatch ) print "Top level snapshot: " + str( self.__fsTopLevelSnapshot ) print "Project snapshot: " + str( self.__fsSnapshot )
class ProjectWidget(Ui_Form, QWidget): SampleWidgetRole = Qt.UserRole + 1 projectsaved = pyqtSignal() projectupdated = pyqtSignal() projectloaded = pyqtSignal(object) selectlayersupdated = pyqtSignal(list) def __init__(self, parent=None): super(ProjectWidget, self).__init__(parent) self.setupUi(self) self.project = None self.mapisloaded = False self.bar = None self.canvas.setCanvasColor(Qt.white) self.canvas.enableAntiAliasing(True) self.canvas.setWheelAction(QgsMapCanvas.WheelZoomToMouseCursor) self.canvas.mapRenderer().setLabelingEngine(QgsPalLabeling()) self.fieldsmodel = QgsFieldModel() self.widgetmodel = WidgetsModel() self.possiblewidgetsmodel = QStandardItemModel() self.formlayersmodel = QgsLayerModel(watchregistry=False) self.formlayers = CaptureLayerFilter() self.formlayers.setSourceModel(self.formlayersmodel) self.selectlayermodel = CaptureLayersModel(watchregistry=False) self.selectlayerfilter = LayerTypeFilter() self.selectlayerfilter.setSourceModel(self.selectlayermodel) self.selectlayermodel.dataChanged.connect(self.selectlayerschanged) self.layerCombo.setModel(self.formlayers) self.widgetCombo.setModel(self.possiblewidgetsmodel) self.selectLayers.setModel(self.selectlayerfilter) self.selectLayers_2.setModel(self.selectlayerfilter) self.fieldList.setModel(self.fieldsmodel) self.widgetlist.setModel(self.widgetmodel) self.widgetlist.selectionModel().currentChanged.connect(self.updatecurrentwidget) self.widgetmodel.rowsRemoved.connect(self.setwidgetconfigvisiable) self.widgetmodel.rowsInserted.connect(self.setwidgetconfigvisiable) self.widgetmodel.modelReset.connect(self.setwidgetconfigvisiable) self.titleText.textChanged.connect(self.updatetitle) QgsProject.instance().readProject.connect(self._readproject) self.loadwidgettypes() self.addWidgetButton.pressed.connect(self.newwidget) self.removeWidgetButton.pressed.connect(self.removewidget) self.roamVersionLabel.setText("You are running IntraMaps Roam version {}".format(roam.__version__)) self.openProjectFolderButton.pressed.connect(self.openprojectfolder) self.openinQGISButton.pressed.connect(self.openinqgis) self.filewatcher = QFileSystemWatcher() self.filewatcher.fileChanged.connect(self.qgisprojectupdated) self.formfolderLabel.linkActivated.connect(self.openformfolder) self.projectupdatedlabel.linkActivated.connect(self.reloadproject) self.projectupdatedlabel.hide() self.formtab.currentChanged.connect(self.formtabchanged) self.expressionButton.clicked.connect(self.opendefaultexpression) self.fieldList.currentIndexChanged.connect(self.updatewidgetname) self.fieldwarninglabel.hide() for item, data in readonlyvalues: self.readonlyCombo.addItem(item, data) self.setpage(4) self.form = None def setaboutinfo(self): self.versionLabel.setText(roam.__version__) self.qgisapiLabel.setText(str(QGis.QGIS_VERSION)) def checkcapturelayers(self): haslayers = self.project.hascapturelayers() self.formslayerlabel.setVisible(not haslayers) return haslayers def opendefaultexpression(self): layer = self.currentform.QGISLayer dlg = QgsExpressionBuilderDialog(layer, "Create default value expression", self) text = self.defaultvalueText.text().strip('[%').strip('%]').strip() dlg.setExpressionText(text) if dlg.exec_(): self.defaultvalueText.setText('[% {} %]'.format(dlg.expressionText())) def openformfolder(self, url): openfolder(url) def selectlayerschanged(self, *args): self.formlayers.setSelectLayers(self.project.selectlayers) self.checkcapturelayers() self.selectlayersupdated.emit(self.project.selectlayers) def formtabchanged(self, index): # preview if index == 1: self.form.settings['widgets'] = list(self.widgetmodel.widgets()) self.setformpreview(self.form) def setpage(self, page): self.stackedWidget.setCurrentIndex(page) def reloadproject(self, *args): self.setproject(self.project) def qgisprojectupdated(self, path): self.projectupdatedlabel.show() self.projectupdatedlabel.setText("The QGIS project has been updated. <a href='reload'> " "Click to reload</a>. <b style=\"color:red\">Unsaved data will be lost</b>") def openinqgis(self): projectfile = self.project.projectfile qgislocation = r'C:\OSGeo4W\bin\qgis.bat' qgislocation = roam.config.settings.setdefault('configmanager', {}) \ .setdefault('qgislocation', qgislocation) try: openqgis(projectfile, qgislocation) except WindowsError: self.bar.pushMessage("Looks like I couldn't find QGIS", "Check qgislocation in roam.config", QgsMessageBar.WARNING) def openprojectfolder(self): folder = self.project.folder openfolder(folder) def setwidgetconfigvisiable(self, *args): haswidgets = self.widgetmodel.rowCount() > 0 self.widgetframe.setEnabled(haswidgets) def removewidget(self): """ Remove the selected widget from the widgets list """ widget, index = self.currentuserwidget if index.isValid(): self.widgetmodel.removeRow(index.row(), index.parent()) def newwidget(self): """ Create a new widget. The default is a list. """ widget = {} widget['widget'] = 'Text' # Grab the first field. widget['field'] = self.fieldsmodel.index(0, 0).data(QgsFieldModel.FieldNameRole) currentindex = self.widgetlist.currentIndex() currentitem = self.widgetmodel.itemFromIndex(currentindex) if currentitem and currentitem.iscontainor(): parent = currentindex else: parent = currentindex.parent() index = self.widgetmodel.addwidget(widget, parent) self.widgetlist.setCurrentIndex(index) def loadwidgettypes(self): self.widgetCombo.blockSignals(True) for widgettype in roam.editorwidgets.core.supportedwidgets(): try: configclass = configmanager.editorwidgets.widgetconfigs[widgettype] except KeyError: continue configwidget = configclass() item = QStandardItem(widgettype) item.setData(configwidget, Qt.UserRole) item.setData(widgettype, Qt.UserRole + 1) item.setIcon(QIcon(widgeticon(widgettype))) self.widgetCombo.model().appendRow(item) self.widgetstack.addWidget(configwidget) self.widgetCombo.blockSignals(False) def usedfields(self): """ Return the list of fields that have been used by the the current form's widgets """ for widget in self.currentform.widgets: yield widget['field'] @property def currentform(self): """ Return the current selected form. """ return self.form @property def currentuserwidget(self): """ Return the selected user widget. """ index = self.widgetlist.currentIndex() return index.data(Qt.UserRole), index @property def currentwidgetconfig(self): """ Return the selected widget in the widget combo. """ index = self.widgetCombo.currentIndex() index = self.possiblewidgetsmodel.index(index, 0) return index.data(Qt.UserRole), index, index.data(Qt.UserRole + 1) def updatewidgetname(self, index): # Only change the edit text on name field if it's not already set to something other then the # field name. field = self.fieldsmodel.index(index, 0).data(QgsFieldModel.FieldNameRole) currenttext = self.nameText.text() foundfield = self.fieldsmodel.findfield(currenttext) if foundfield: self.nameText.setText(field) def _save_widgetfield(self, index): """ Save the selected field for the current widget. Shows a error if the field is already used but will allow the user to still set it in the case of extra logic for that field in the forms Python logic. """ widget, index = self.currentuserwidget row = self.fieldList.currentIndex() field = self.fieldsmodel.index(row, 0).data(QgsFieldModel.FieldNameRole) showwarning = field in self.usedfields() self.fieldwarninglabel.setVisible(showwarning) widget['field'] = field self.widgetmodel.setData(index, widget, Qt.UserRole) def _save_selectedwidget(self, index): configwidget, index, widgettype = self.currentwidgetconfig widget, index = self.currentuserwidget if not widget: return widget['widget'] = widgettype widget['required'] = self.requiredCheck.isChecked() widget['config'] = configwidget.getconfig() widget['name'] = self.nameText.text() widget['read-only-rules'] = [self.readonlyCombo.itemData(self.readonlyCombo.currentIndex())] widget['hidden'] = self.hiddenCheck.isChecked() self.widgetmodel.setData(index, widget, Qt.UserRole) def _save_default(self): widget, index = self.currentuserwidget default = self.defaultvalueText.text() widget['default'] = default self.widgetmodel.setData(index, widget, Qt.UserRole) def _save_selectionlayers(self, index, layer, value): config = self.project.settings self.selectlayermodel.dataChanged.emit(index, index) def _save_formtype(self, index): formtype = self.formtypeCombo.currentText() form = self.currentform form.settings['type'] = formtype def _save_formname(self, text): """ Save the form label to the settings file. """ try: form = self.currentform if form is None: return form.settings['label'] = text self.projectupdated.emit() except IndexError: return def _save_layer(self, index): """ Save the selected layer to the settings file. """ index = self.formlayers.index(index, 0) layer = index.data(Qt.UserRole) if not layer: return form = self.currentform if form is None: return form.settings['layer'] = layer.name() self.updatefields(layer) def setsplash(self, splash): pixmap = QPixmap(splash) w = self.splashlabel.width() h = self.splashlabel.height() self.splashlabel.setPixmap(pixmap.scaled(w,h, Qt.KeepAspectRatio)) def setproject(self, project, loadqgis=True): """ Set the widgets active project. """ self.disconnectsignals() self.mapisloaded = False self.filewatcher.removePaths(self.filewatcher.files()) self.projectupdatedlabel.hide() self._closeqgisproject() if project.valid: self.startsettings = copy.deepcopy(project.settings) self.project = project self.projectlabel.setText(project.name) self.versionText.setText(project.version) self.selectlayermodel.config = project.settings self.formlayers.setSelectLayers(self.project.selectlayers) self.setsplash(project.splash) self.loadqgisproject(project, self.project.projectfile) self.filewatcher.addPath(self.project.projectfile) self.projectloaded.emit(self.project) def loadqgisproject(self, project, projectfile): QDir.setCurrent(os.path.dirname(project.projectfile)) fileinfo = QFileInfo(project.projectfile) QgsProject.instance().read(fileinfo) def _closeqgisproject(self): if self.canvas.isDrawing(): return self.canvas.freeze(True) self.formlayersmodel.removeall() self.selectlayermodel.removeall() QgsMapLayerRegistry.instance().removeAllMapLayers() self.canvas.freeze(False) def loadmap(self): if self.mapisloaded: return # This is a dirty hack to work around the timer that is in QgsMapCanvas in 2.2. # Refresh will stop the canvas timer # Repaint will redraw the widget. # loadmap is only called once per project load so it's safe to do this here. self.canvas.refresh() self.canvas.repaint() parser = roam.projectparser.ProjectParser.fromFile(self.project.projectfile) canvasnode = parser.canvasnode self.canvas.mapRenderer().readXML(canvasnode) self.canvaslayers = parser.canvaslayers() self.canvas.setLayerSet(self.canvaslayers) self.canvas.updateScale() self.canvas.refresh() self.mapisloaded = True def _readproject(self, doc): self.formlayersmodel.refresh() self.selectlayermodel.refresh() self._updateforproject(self.project) def _updateforproject(self, project): self.titleText.setText(project.name) self.descriptionText.setPlainText(project.description) def swapwidgetconfig(self, index): widgetconfig, _, _ = self.currentwidgetconfig defaultvalue = widgetconfig.defaultvalue self.defaultvalueText.setText(defaultvalue) self.updatewidgetconfig({}) def updatetitle(self, text): self.project.settings['title'] = text self.projectlabel.setText(text) self.projectupdated.emit() def updatewidgetconfig(self, config): widgetconfig, index, widgettype = self.currentwidgetconfig self.setconfigwidget(widgetconfig, config) def setformpreview(self, form): def removewidget(): item = self.frame_2.layout().itemAt(0) if item and item.widget(): item.widget().setParent(None) removewidget() featureform = FeatureForm.from_form(form, form.settings, None, {}) self.frame_2.layout().addWidget(featureform) def connectsignals(self): self.formLabelText.textChanged.connect(self._save_formname) self.layerCombo.currentIndexChanged.connect(self._save_layer) self.formtypeCombo.currentIndexChanged.connect(self._save_formtype) #widget settings self.fieldList.currentIndexChanged.connect(self._save_widgetfield) self.requiredCheck.toggled.connect(self._save_selectedwidget) self.defaultvalueText.textChanged.connect(self._save_default) self.widgetCombo.currentIndexChanged.connect(self._save_selectedwidget) self.widgetCombo.currentIndexChanged.connect(self.swapwidgetconfig) self.nameText.textChanged.connect(self._save_selectedwidget) self.readonlyCombo.currentIndexChanged.connect(self._save_selectedwidget) self.hiddenCheck.toggled.connect(self._save_selectedwidget) def disconnectsignals(self): try: self.formLabelText.textChanged.disconnect(self._save_formname) self.layerCombo.currentIndexChanged.disconnect(self._save_layer) self.formtypeCombo.currentIndexChanged.disconnect(self._save_formtype) #widget settings self.fieldList.currentIndexChanged.disconnect(self._save_widgetfield) self.requiredCheck.toggled.disconnect(self._save_selectedwidget) self.defaultvalueText.textChanged.disconnect(self._save_default) self.widgetCombo.currentIndexChanged.disconnect(self._save_selectedwidget) self.widgetCombo.currentIndexChanged.disconnect(self.swapwidgetconfig) self.nameText.textChanged.disconnect(self._save_selectedwidget) self.readonlyCombo.currentIndexChanged.disconnect(self._save_selectedwidget) self.hiddenCheck.toggled.disconnect(self._save_selectedwidget) except TypeError: pass def setform(self, form): """ Update the UI with the currently selected form. """ def getfirstlayer(): index = self.formlayers.index(0,0) layer = index.data(Qt.UserRole) layer = layer.name() return layer def loadwidgets(widget): """ Load the widgets into widgets model """ self.widgetmodel.clear() self.widgetmodel.loadwidgets(form.widgets) def findlayer(layername): index = self.formlayersmodel.findlayer(layername) index = self.formlayers.mapFromSource(index) layer = index.data(Qt.UserRole) return index, layer self.disconnectsignals() self.form = form settings = form.settings label = form.label layername = settings.setdefault('layer', getfirstlayer()) layerindex, layer = findlayer(layername) if not layer or not layerindex.isValid(): return formtype = settings.setdefault('type', 'auto') widgets = settings.setdefault('widgets', []) self.formLabelText.setText(label) folderurl = "<a href='{path}'>{name}</a>".format(path=form.folder, name=os.path.basename(form.folder)) self.formfolderLabel.setText(folderurl) self.layerCombo.setCurrentIndex(layerindex.row()) self.updatefields(layer) index = self.formtypeCombo.findText(formtype) if index == -1: self.formtypeCombo.insertItem(0, formtype) self.formtypeCombo.setCurrentIndex(0) else: self.formtypeCombo.setCurrentIndex(index) loadwidgets(widgets) # Set the first widget index = self.widgetmodel.index(0, 0) if index.isValid(): self.widgetlist.setCurrentIndex(index) self.updatecurrentwidget(index, None) self.connectsignals() def updatefields(self, layer): """ Update the UI with the fields for the selected layer. """ self.fieldsmodel.setLayer(layer) def setconfigwidget(self, configwidget, config): """ Set the active config widget. """ try: configwidget.widgetdirty.disconnect(self._save_selectedwidget) except TypeError: pass #self.descriptionLabel.setText(configwidget.description) self.widgetstack.setCurrentWidget(configwidget) configwidget.setconfig(config) configwidget.widgetdirty.connect(self._save_selectedwidget) def updatecurrentwidget(self, index, _): """ Update the UI with the config for the current selected widget. """ if not index.isValid(): return widget = index.data(Qt.UserRole) widgettype = widget['widget'] field = widget['field'] required = widget.setdefault('required', False) name = widget.setdefault('name', field) default = widget.setdefault('default', '') readonly = widget.setdefault('read-only-rules', []) hidden = widget.setdefault('hidden', False) try: data = readonly[0] except: data = 'never' self.readonlyCombo.blockSignals(True) index = self.readonlyCombo.findData(data) self.readonlyCombo.setCurrentIndex(index) self.readonlyCombo.blockSignals(False) self.defaultvalueText.blockSignals(True) if not isinstance(default, dict): self.defaultvalueText.setText(default) self.defaultvalueText.setEnabled(True) self.expressionButton.setEnabled(True) else: # TODO Handle the more advanced default values. self.defaultvalueText.setText("Advanced default set in config") self.defaultvalueText.setEnabled(False) self.expressionButton.setEnabled(False) self.defaultvalueText.blockSignals(False) self.nameText.blockSignals(True) self.nameText.setText(name) self.nameText.blockSignals(False) self.requiredCheck.blockSignals(True) self.requiredCheck.setChecked(required) self.requiredCheck.blockSignals(False) self.hiddenCheck.blockSignals(True) self.hiddenCheck.setChecked(hidden) self.hiddenCheck.blockSignals(False) if not field is None: self.fieldList.blockSignals(True) index = self.fieldList.findData(field.lower(), QgsFieldModel.FieldNameRole) if index > -1: self.fieldList.setCurrentIndex(index) else: self.fieldList.setEditText(field) self.fieldList.blockSignals(False) index = self.widgetCombo.findText(widgettype) self.widgetCombo.blockSignals(True) if index > -1: self.widgetCombo.setCurrentIndex(index) self.widgetCombo.blockSignals(False) self.updatewidgetconfig(config=widget.setdefault('config', {})) def _saveproject(self): """ Save the project config to disk. """ title = self.titleText.text() description = self.descriptionText.toPlainText() version = str(self.versionText.text()) settings = self.project.settings settings['title'] = title settings['description'] = description settings['version'] = version form = self.currentform if form: form.settings['widgets'] = list(self.widgetmodel.widgets()) logger.debug(form.settings) self.project.save() self.projectsaved.emit()
class Watcher(QObject): " Filesystem watcher implementation " fsChanged = pyqtSignal(list) def __init__(self, excludeFilters, dirToWatch): QObject.__init__(self) self.__dirWatcher = QFileSystemWatcher(self) # data members self.__excludeFilter = [] # Files exclude filter self.__srcDirsToWatch = set() # Came from the user self.__fsTopLevelSnapshot = {} # Current snapshot self.__fsSnapshot = {} # Current snapshot # Sets of dirs which are currently watched self.__dirsToWatch = set() self.__topLevelDirsToWatch = set() # Generated till root # precompile filters for flt in excludeFilters: self.__excludeFilter.append(re.compile(flt)) # Initialise the list of dirs to watch self.__srcDirsToWatch.add(dirToWatch) self.__topLevelDirsToWatch = self.__buildTopDirsList( self.__srcDirsToWatch) self.__fsTopLevelSnapshot = self.__buildTopLevelSnapshot( self.__topLevelDirsToWatch, self.__srcDirsToWatch) self.__dirsToWatch = self.__buildSnapshot() # Here __dirsToWatch and __topLevelDirsToWatch have a complete # set of what should be watched # Add the dirs to the watcher dirs = [] for path in self.__dirsToWatch | self.__topLevelDirsToWatch: dirs.append(path) self.__dirWatcher.addPaths(dirs) self.__dirWatcher.directoryChanged.connect(self.__onDirChanged) # self.debug() return @staticmethod def __buildTopDirsList(srcDirs): " Takes a list of dirs to be watched and builds top dirs set " topDirsList = set() for path in srcDirs: parts = path.split(os.path.sep) for index in xrange(1, len(parts) - 1): candidate = os.path.sep.join(parts[0:index]) + os.path.sep if os.path.exists(candidate): if os.access(candidate, os.R_OK): topDirsList.add(candidate) return topDirsList @staticmethod def __buildTopLevelSnapshot(topLevelDirs, srcDirs): " Takes top level dirs and builds their snapshot " snapshot = {} for path in topLevelDirs: itemsSet = set() # search for all the dirs to be watched for candidate in topLevelDirs | srcDirs: if len(candidate) <= len(path): continue if candidate.startswith(path): candidate = candidate[len(path):] slashIndex = candidate.find(os.path.sep) + 1 item = candidate[:slashIndex] if os.path.exists(path + item): itemsSet.add(item) snapshot[path] = itemsSet return snapshot def __buildSnapshot(self): " Builds the filesystem snapshot " snapshotDirs = set() for path in self.__srcDirsToWatch: self.__addSnapshotPath(path, snapshotDirs) return snapshotDirs def __addSnapshotPath(self, path, snapshotDirs, itemsToReport=None): " Adds one path to the FS snapshot " if not os.path.exists(path): return snapshotDirs.add(path) dirItems = set() for item in os.listdir(path): if self.__shouldExclude(item): continue if os.path.isdir(path + item): dirName = path + item + os.path.sep dirItems.add(item + os.path.sep) if itemsToReport is not None: itemsToReport.append("+" + dirName) self.__addSnapshotPath(dirName, snapshotDirs, itemsToReport) continue dirItems.add(item) if itemsToReport is not None: itemsToReport.append("+" + path + item) self.__fsSnapshot[path] = dirItems return def __onDirChanged(self, path): " Triggered when the dir is changed " path = str(path) if not path.endswith(os.path.sep): path = path + os.path.sep # Check if it is a top level dir try: oldSet = self.__fsTopLevelSnapshot[path] # Build a new set of what is in that top level dir newSet = set() for item in os.listdir(path): if not os.path.isdir(path + item): continue # Only dirs are of interest for the top level item = item + os.path.sep if item in oldSet: newSet.add(item) # Now we have an old set and a new one with those from the old # which actually exist diff = oldSet - newSet # diff are those which disappeared. We need to do the following: # - build a list of all the items in the fs snapshot which start # from this dir # - build a list of dirs which should be deregistered from the # watcher. This list includes both top level and project level # - deregister dirs from the watcher # - emit a signal of what disappeared if not diff: return # no changes self.__fsTopLevelSnapshot[path] = newSet dirsToBeRemoved = [] itemsToReport = [] for item in diff: self.__processRemoveTopDir(path + item, dirsToBeRemoved, itemsToReport) # Here: it is possible that the last dir to watch disappeared if not newSet: # There is nothing to watch here anymore dirsToBeRemoved.append(path) del self.__fsTopLevelSnapshot[path] parts = path[1:-1].split(os.path.sep) for index in xrange(len(parts) - 2, 0, -1): candidate = os.path.sep + \ os.path.sep.join( parts[ 0 : index ] ) + \ os.path.sep dirSet = self.__fsTopLevelSnapshot[candidate] dirSet.remove(parts[index + 1] + os.path.sep) if not dirSet: dirsToBeRemoved.append(candidate) del self.__fsTopLevelSnapshot[candidate] continue break # it is not the last item in the set # Update the watcher if dirsToBeRemoved: self.__dirWatcher.removePaths(dirsToBeRemoved) # Report if itemsToReport: self.fsChanged.emit(itemsToReport) return except: # it is not a top level dir - no key pass # Here: the change is in the project level dir try: oldSet = self.__fsSnapshot[path] # Build a new set of what is in that top level dir newSet = set() for item in os.listdir(path): if self.__shouldExclude(item): continue if os.path.isdir(path + item): newSet.add(item + os.path.sep) else: newSet.add(item) # Here: we have a new and old snapshots # Lets calculate the difference deletedItems = oldSet - newSet addedItems = newSet - oldSet if not deletedItems and not addedItems: return # No changes # Update the changed dir set self.__fsSnapshot[path] = newSet # We need to build some lists: # - list of files which were added # - list of dirs which were added # - list of files which were deleted # - list of dirs which were deleted # The deleted dirs must be unregistered in the watcher # The added dirs must be registered itemsToReport = [] dirsToBeAdded = [] dirsToBeRemoved = [] for item in addedItems: if item.endswith(os.path.sep): # directory was added self.__processAddedDir(path + item, dirsToBeAdded, itemsToReport) else: itemsToReport.append("+" + path + item) for item in deletedItems: if item.endswith(os.path.sep): # directory was deleted self.__processRemovedDir(path + item, dirsToBeRemoved, itemsToReport) else: itemsToReport.append("-" + path + item) # Update the watcher if dirsToBeRemoved: self.__dirWatcher.removePaths(dirsToBeRemoved) if dirsToBeAdded: self.__dirWatcher.addPaths(dirsToBeAdded) # Report self.fsChanged.emit(itemsToReport) except: # It could be a queued signal about what was already reported pass # self.debug() return def __shouldExclude(self, name): " Tests if a file must be excluded " for excl in self.__excludeFilter: if excl.match(name): return True return False def __processAddedDir(self, path, dirsToBeAdded, itemsToReport): " called for an appeared dir in the project tree " dirsToBeAdded.append(path) itemsToReport.append("+" + path) # it should add dirs recursively into the snapshot and care # of the items to report dirItems = set() for item in os.listdir(path): if self.__shouldExclude(item): continue if os.path.isdir(path + item): dirName = path + item + os.path.sep dirItems.add(item + os.path.sep) self.__processAddedDir(dirName, dirsToBeAdded, itemsToReport) continue itemsToReport.append("+" + path + item) dirItems.add(item) self.__fsSnapshot[path] = dirItems return def __processRemovedDir(self, path, dirsToBeRemoved, itemsToReport): " called for a disappeared dir in the project tree " # it should remove the dirs recursively from the fs snapshot # and care of items to report dirsToBeRemoved.append(path) itemsToReport.append("-" + path) oldSet = self.__fsSnapshot[path] for item in oldSet: if item.endswith(os.path.sep): # Nested dir self.__processRemovedDir(path + item, dirsToBeRemoved, itemsToReport) else: # a file itemsToReport.append("-" + path + item) del self.__fsSnapshot[path] return def __processRemoveTopDir(self, path, dirsToBeRemoved, itemsToReport): " Called for a disappeared top level dir " if path in self.__fsTopLevelSnapshot: # It is still a top level dir dirsToBeRemoved.append(path) for item in self.__fsTopLevelSnapshot[path]: self.__processRemoveTopDir(path + item, dirsToBeRemoved, itemsToReport) del self.__fsTopLevelSnapshot[path] else: # This is a project level dir self.__processRemovedDir(path, dirsToBeRemoved, itemsToReport) return def reset(self): " Resets the watcher (it does not report any changes) " self.__dirWatcher.removePaths(self.__dirWatcher.directories()) self.__srcDirsToWatch = set() self.__fsTopLevelSnapshot = {} self.__fsSnapshot = {} self.__dirsToWatch = set() self.__topLevelDirsToWatch = set() return def registerDir(self, path): " Adds a directory to the list of watched ones " if not path.endswith(os.path.sep): path = path + os.path.sep if path in self.__srcDirsToWatch: return # It is there already # It is necessary to do the following: # - add the dir to the fs snapshot # - collect dirs to add to the watcher # - collect items to report self.__srcDirsToWatch.add(path) dirsToWatch = set() itemsToReport = [] self.__registerDir(path, dirsToWatch, itemsToReport) # It might be that top level dirs should be updated too newTopLevelDirsToWatch = self.__buildTopDirsList(self.__srcDirsToWatch) addedDirs = newTopLevelDirsToWatch - self.__topLevelDirsToWatch for item in addedDirs: dirsToWatch.add(item) # Identify items to be watched by this dir dirItems = set() for candidate in newTopLevelDirsToWatch | self.__srcDirsToWatch: if len(candidate) <= len(item): continue if candidate.startswith(item): candidate = candidate[len(item):] slashIndex = candidate.find(os.path.sep) + 1 dirName = candidate[:slashIndex] if os.path.exists(item + dirName): dirItems.add(dirName) # Update the top level dirs snapshot self.__fsTopLevelSnapshot[item] = dirItems # Update the top level snapshot with the added dir upperDir = os.path.dirname(path[:-1]) + os.path.sep dirName = path.replace(upperDir, '') self.__fsTopLevelSnapshot[upperDir].add(dirName) # Update the list of top level dirs to watch self.__topLevelDirsToWatch = newTopLevelDirsToWatch # Update the watcher if dirsToWatch: dirs = [] for item in dirsToWatch: dirs.append(item) self.__dirWatcher.addPaths(dirs) # Report the changes if itemsToReport: self.fsChanged.emit(itemsToReport) # self.debug() return def __registerDir(self, path, dirsToWatch, itemsToReport): " Adds one path to the FS snapshot " if not os.path.exists(path): return dirsToWatch.add(path) itemsToReport.append("+" + path) dirItems = set() for item in os.listdir(path): if self.__shouldExclude(item): continue if os.path.isdir(path + item): dirName = path + item + os.path.sep dirItems.add(item + os.path.sep) itemsToReport.append("+" + path + item + os.path.sep) self.__addSnapshotPath(dirName, dirsToWatch, itemsToReport) continue dirItems.add(item) itemsToReport.append("+" + path + item) self.__fsSnapshot[path] = dirItems return def deregisterDir(self, path): " Removes the directory from the list of the watched ones " if not path.endswith(os.path.sep): path = path + os.path.sep if path not in self.__srcDirsToWatch: return # It is not there already self.__srcDirsToWatch.remove(path) # It is necessary to do the following: # - remove the dir from the fs snapshot # - collect the dirs to be removed from watching # - collect item to report itemsToReport = [] dirsToBeRemoved = [] self.__deregisterDir(path, dirsToBeRemoved, itemsToReport) # It is possible that some of the top level watched dirs should be # removed as well newTopLevelDirsToWatch = self.__buildTopDirsList(self.__srcDirsToWatch) deletedDirs = self.__topLevelDirsToWatch - newTopLevelDirsToWatch for item in deletedDirs: dirsToBeRemoved.append(item) del self.__fsTopLevelSnapshot[item] # It might be the case that some of the items should be deleted in the # top level dirs sets for dirName in self.__fsTopLevelSnapshot: itemsSet = self.__fsTopLevelSnapshot[dirName] for item in itemsSet: candidate = dirName + item if candidate == path or candidate in deletedDirs: itemsSet.remove(item) self.__fsTopLevelSnapshot[dirName] = itemsSet break # Update the list of dirs to be watched self.__topLevelDirsToWatch = newTopLevelDirsToWatch # Update the watcher if dirsToBeRemoved: self.__dirWatcher.removePaths(dirsToBeRemoved) # Report the changes if itemsToReport: self.fsChanged.emit(itemsToReport) # self.debug() return def __deregisterDir(self, path, dirsToBeRemoved, itemsToReport): " Deregisters a directory recursively " dirsToBeRemoved.append(path) itemsToReport.append("-" + path) if path in self.__fsTopLevelSnapshot: # This is a top level dir for item in self.__fsTopLevelSnapshot[path]: if item.endswith(os.path.sep): # It's a dir self.__deregisterDir(path + item, dirsToBeRemoved, itemsToReport) else: # It's a file itemsToReport.append("-" + path + item) del self.__fsTopLevelSnapshot[path] return # It is from an a project level snapshot if path in self.__fsSnapshot: for item in self.__fsSnapshot[path]: if item.endswith(os.path.sep): # It's a dir self.__deregisterDir(path + item, dirsToBeRemoved, itemsToReport) else: # It's a file itemsToReport.append("-" + path + item) del self.__fsSnapshot[path] return def debug(self): print "Top level dirs to watch: " + str(self.__topLevelDirsToWatch) print "Project dirs to watch: " + str(self.__dirsToWatch) print "Top level snapshot: " + str(self.__fsTopLevelSnapshot) print "Project snapshot: " + str(self.__fsSnapshot)
class _FileWatcher(QObject): """File watcher. QFileSystemWatcher notifies client about any change (file access mode, modification date, etc.) But, we need signal, only after file contents had been changed """ modified = pyqtSignal(bool) removed = pyqtSignal(bool) def __init__(self, path): QObject.__init__(self) self._contents = None self._watcher = QFileSystemWatcher() self._timer = None self._path = path self.setPath(path) self.enable() def __del__(self): self._stopTimer() def enable(self): """Enable signals from the watcher """ self._watcher.fileChanged.connect(self._onFileChanged) def disable(self): """Disable signals from the watcher """ self._watcher.fileChanged.disconnect(self._onFileChanged) self._stopTimer() def setContents(self, contents): """Set file contents. Watcher uses it to compare old and new contents of the file. """ self._contents = contents # Qt File watcher may work incorrectly, if file was not existing, when it started self.setPath(self._path) def setPath(self, path): """Path had been changed or file had been created. Set new path """ if self._watcher.files(): self._watcher.removePaths(self._watcher.files()) if path is not None and os.path.isfile(path): self._watcher.addPath(path) self._path = path def _emitModifiedStatus(self): """Emit self.modified signal with right status """ isModified = self._contents != self._safeRead(self._path) self.modified.emit(isModified) def _onFileChanged(self): """File changed. Emit own signal, if contents changed """ if os.path.exists(self._path): self._emitModifiedStatus() else: self.removed.emit(True) self._startTimer() def _startTimer(self): """Init a timer. It is used for monitoring file after deletion. Git removes file, than restores it. """ if self._timer is None: self._timer = QTimer() self._timer.setInterval(500) self._timer.timeout.connect(self._onCheckIfDeletedTimer) self._timer.start() def _stopTimer(self): """Stop timer, if exists """ if self._timer is not None: self._timer.stop() def _onCheckIfDeletedTimer(self): """Check, if file has been restored """ if os.path.exists(self._path): self.removed.emit(False) self._emitModifiedStatus() self.setPath( self._path ) # restart Qt file watcher after file has been restored self._stopTimer() def _safeRead(self, path): """Read file. Ignore exceptions """ try: with open(path, 'rb') as file: return file.read() except (OSError, IOError): return None
class ProjectWidget(Ui_Form, QWidget): SampleWidgetRole = Qt.UserRole + 1 projectsaved = pyqtSignal() projectupdated = pyqtSignal(object) projectloaded = pyqtSignal(object) selectlayersupdated = pyqtSignal(list) def __init__(self, parent=None): super(ProjectWidget, self).__init__(parent) self.setupUi(self) self.project = None self.bar = None self.roamapp = None menu = QMenu() # self.roamVersionLabel.setText("You are running IntraMaps Roam version {}".format(roam.__version__)) self.openProjectFolderButton.pressed.connect(self.openprojectfolder) self.openinQGISButton.pressed.connect(self.openinqgis) self.depolyProjectButton.pressed.connect(self.deploy_project) self.depolyInstallProjectButton.pressed.connect(functools.partial(self.deploy_project, True)) self.filewatcher = QFileSystemWatcher() self.filewatcher.fileChanged.connect(self.qgisprojectupdated) self.projectupdatedlabel.linkActivated.connect(self.reloadproject) self.projectupdatedlabel.hide() # self.setpage(4) self.currentnode = None self.form = None qgislocation = r'C:\OSGeo4W\bin\qgis.bat' qgislocation = roam.config.settings.setdefault('configmanager', {}) \ .get('qgislocation', "") self.qgispathEdit.setText(qgislocation) self.qgispathEdit.textChanged.connect(self.save_qgis_path) self.filePickerButton.pressed.connect(self.set_qgis_path) self.connect_page_events() QgsProject.instance().readProject.connect(self.projectLoaded) def connect_page_events(self): """ Connect the events from all the pages back to here """ for index in range(self.stackedWidget.count()): widget = self.stackedWidget.widget(index) if hasattr(widget, "raiseMessage"): widget.raiseMessage.connect(self.bar.pushMessage) def set_qgis_path(self): """ Set the location of the QGIS install. We need the path to be able to create Roam projects """ path = QFileDialog.getOpenFileName(self, "Select QGIS install file", filter="(*.bat)") if not path: return self.qgispathEdit.setText(path) self.save_qgis_path(path) def save_qgis_path(self, path): """ Save the QGIS path back to the Roam config. """ roam.config.settings['configmanager'] = {'qgislocation': path} roam.config.save() def setpage(self, page, node, refreshingProject=False): """ Set the current page in the config manager. We pass the project into the current page so that it knows what the project is. """ self.currentnode = node if not refreshingProject and self.project: self.write_config_currentwidget() else: roam.utils.info("Reloading project. Not saving current config values") self.unload_current_widget() # Set the new widget for the selected page self.stackedWidget.setCurrentIndex(page) widget = self.stackedWidget.currentWidget() print "New widget {}".format(widget.objectName()) if hasattr(widget, "set_project"): widget.set_project(self.project, self.currentnode) def unload_current_widget(self): print "NOTIFY!!" widget = self.stackedWidget.currentWidget() print widget if hasattr(widget, "unload_project"): widget.unload_project() def write_config_currentwidget(self): """ Call the write config command on the current widget. """ widget = self.stackedWidget.currentWidget() if hasattr(widget, "write_config"): roam.utils.debug("Write config for {} in project {}".format(widget.objectName(), self.project.name)) widget.write_config() def deploy_project(self, with_data=False): """ Run the step to deploy a project. Projects are deplyed as a bundled zip of the project folder. """ if self.roamapp.sourcerun: base = os.path.join(self.roamapp.apppath, "..") else: base = self.roamapp.apppath default = os.path.join(base, "roam_serv") path = roam.config.settings.get("publish", {}).get("path", '') if not path: path = default path = os.path.join(path, "projects") if not os.path.exists(path): os.makedirs(path) self._saveproject(update_version=True, reset_save_point=True) options = {} bundle.bundle_project(self.project, path, options, as_install=with_data) def setaboutinfo(self): """ Set the current about info on the widget """ self.versionLabel.setText(roam.__version__) self.qgisapiLabel.setText(unicode(QGis.QGIS_VERSION)) def selectlayerschanged(self, *args): """ Run the updates when the selection layers have changed """ self.formlayers.setSelectLayers(self.project.selectlayers) self.selectlayersupdated.emit(self.project.selectlayers) def reloadproject(self, *args): """ Reload the project. At the moment this will drop any unsaved changes to the config. Note: Should look at making sure it doesn't do that because it's not really needed. """ self.projectupdated.emit(self.project) # self.setproject(self.project) def qgisprojectupdated(self, path): """ Show a message when the QGIS project file has been updated. """ self.projectupdatedlabel.show() self.projectupdatedlabel.setText("The QGIS project has been updated. <a href='reload'> " "Click to reload</a>. <b style=\"color:red\">Unsaved data will be lost</b>") def openinqgis(self): """ Open a QGIS session for the user to config the project layers. """ try: openqgis(self.project.projectfile) except OSError: self.bar.pushMessage("Looks like I couldn't find QGIS", "Check qgislocation in roam.config", QgsMessageBar.WARNING) def openprojectfolder(self): """ Open the project folder in the file manager for the OS. """ folder = self.project.folder openfolder(folder) def setproject(self, project): """ Set the widgets active project. """ self.unload_current_widget() if self.project: savelast = QMessageBox.question(self, "Save Current Project", "Save {}?".format(self.project.name), QMessageBox.Save | QMessageBox.No) if savelast == QMessageBox.Accepted: self._saveproject() self.filewatcher.removePaths(self.filewatcher.files()) self.projectupdatedlabel.hide() self._closeqgisproject() if project.valid: self.startsettings = copy.deepcopy(project.settings) self.project = project self.projectlabel.setText(project.name) self.loadqgisproject(project, self.project.projectfile) def projectLoaded(self): self.filewatcher.addPath(self.project.projectfile) self.projectloaded.emit(self.project) def loadqgisproject(self, project, projectfile): QDir.setCurrent(os.path.dirname(project.projectfile)) fileinfo = QFileInfo(project.projectfile) # No idea why we have to set this each time. Maybe QGIS deletes it for # some reason. self.badLayerHandler = BadLayerHandler(callback=self.missing_layers) QgsProject.instance().setBadLayerHandler(self.badLayerHandler) QgsProject.instance().read(fileinfo) def missing_layers(self, missinglayers): """ Handle any and show any missing layers. """ self.project.missing_layers = missinglayers def _closeqgisproject(self): """ Close the current QGIS project and clean up after.. """ QGIS.close_project() def _saveproject(self, update_version=False, reset_save_point=False): """ Save the project config to disk. """ if not self.project: return roam.utils.info("Saving project: {}".format(self.project.name)) self.write_config_currentwidget() # self.project.dump_settings() self.project.save(update_version=update_version, reset_save_point=reset_save_point) self.filewatcher.removePaths(self.filewatcher.files()) QgsProject.instance().write() self.filewatcher.addPath(self.project.projectfile) self.projectsaved.emit()
class ProjectWidget(Ui_Form, QWidget): SampleWidgetRole = Qt.UserRole + 1 projectsaved = pyqtSignal() projectupdated = pyqtSignal() projectloaded = pyqtSignal(object) selectlayersupdated = pyqtSignal(list) def __init__(self, parent=None): super(ProjectWidget, self).__init__(parent) self.setupUi(self) self.project = None self.mapisloaded = False self.bar = None self.roamapp = None menu = QMenu() self.canvas.setCanvasColor(Qt.white) self.canvas.enableAntiAliasing(True) self.canvas.setWheelAction(QgsMapCanvas.WheelZoomToMouseCursor) self.canvas.mapRenderer().setLabelingEngine(QgsPalLabeling()) # self.roamVersionLabel.setText("You are running IntraMaps Roam version {}".format(roam.__version__)) self.openProjectFolderButton.pressed.connect(self.openprojectfolder) self.openinQGISButton.pressed.connect(self.openinqgis) self.depolyProjectButton.pressed.connect(self.deploy_project) self.depolyInstallProjectButton.pressed.connect(functools.partial(self.deploy_project, True)) self.filewatcher = QFileSystemWatcher() self.filewatcher.fileChanged.connect(self.qgisprojectupdated) self.projectupdatedlabel.linkActivated.connect(self.reloadproject) self.projectupdatedlabel.hide() # self.setpage(4) self.currentnode = None self.form = None qgislocation = r'C:\OSGeo4W\bin\qgis.bat' qgislocation = roam.config.settings.setdefault('configmanager', {}) \ .setdefault('qgislocation', qgislocation) self.qgispathEdit.setText(qgislocation) self.qgispathEdit.textChanged.connect(self.save_qgis_path) self.filePickerButton.pressed.connect(self.set_qgis_path) def set_qgis_path(self): path = QFileDialog.getOpenFileName(self, "Select QGIS install file", filter="(*.bat)") if not path: return self.qgispathEdit.setText(path) self.save_qgis_path(path) def save_qgis_path(self, path): roam.config.settings['configmanager'] = {'qgislocation': path} roam.config.save() def setpage(self, page, node): self.currentnode = node self.write_config_currentwidget() self.stackedWidget.setCurrentIndex(page) if self.project: print self.project.dump_settings() widget = self.stackedWidget.currentWidget() if hasattr(widget, "set_project"): widget.set_project(self.project, self.currentnode) def write_config_currentwidget(self): widget = self.stackedWidget.currentWidget() if hasattr(widget, "write_config"): widget.write_config() def deploy_project(self, with_data=False): if self.roamapp.sourcerun: base = os.path.join(self.roamapp.apppath, "..") else: base = self.roamapp.apppath default = os.path.join(base, "roam_serv") path = roam.config.settings.get("publish", {}).get("path", '') if not path: path = default path = os.path.join(path, "projects") if not os.path.exists(path): os.makedirs(path) self._saveproject() options = {} bundle.bundle_project(self.project, path, options, as_install=with_data) def setaboutinfo(self): self.versionLabel.setText(roam.__version__) self.qgisapiLabel.setText(str(QGis.QGIS_VERSION)) def checkcapturelayers(self): haslayers = self.project.hascapturelayers() self.formslayerlabel.setVisible(not haslayers) return haslayers def selectlayerschanged(self, *args): self.formlayers.setSelectLayers(self.project.selectlayers) self.checkcapturelayers() self.selectlayersupdated.emit(self.project.selectlayers) def reloadproject(self, *args): self.setproject(self.project) self.projectupdated.emit() def qgisprojectupdated(self, path): self.projectupdatedlabel.show() self.projectupdatedlabel.setText("The QGIS project has been updated. <a href='reload'> " "Click to reload</a>. <b style=\"color:red\">Unsaved data will be lost</b>") def openinqgis(self): projectfile = self.project.projectfile qgislocation = r'C:\OSGeo4W\bin\qgis.bat' qgislocation = roam.config.settings.setdefault('configmanager', {}) \ .setdefault('qgislocation', qgislocation) try: openqgis(projectfile, qgislocation) except OSError: self.bar.pushMessage("Looks like I couldn't find QGIS", "Check qgislocation in roam.config", QgsMessageBar.WARNING) def openprojectfolder(self): folder = self.project.folder openfolder(folder) def setproject(self, project, loadqgis=True): """ Set the widgets active project. """ self.mapisloaded = False self.filewatcher.removePaths(self.filewatcher.files()) self.projectupdatedlabel.hide() self._closeqgisproject() if project.valid: self.startsettings = copy.deepcopy(project.settings) self.project = project self.projectlabel.setText(project.name) self.loadqgisproject(project, self.project.projectfile) self.filewatcher.addPath(self.project.projectfile) self.projectloaded.emit(self.project) def loadqgisproject(self, project, projectfile): QDir.setCurrent(os.path.dirname(project.projectfile)) fileinfo = QFileInfo(project.projectfile) self.projectLocationLabel.setText("Project File: {}".format(os.path.basename(project.projectfile))) QgsProject.instance().read(fileinfo) def _closeqgisproject(self): if self.canvas.isDrawing(): return self.canvas.freeze(True) QgsMapLayerRegistry.instance().removeAllMapLayers() self.canvas.freeze(False) def loadmap(self): if self.mapisloaded: return # This is a dirty hack to work around the timer that is in QgsMapCanvas in 2.2. # Refresh will stop the canvas timer # Repaint will redraw the widget. # loadmap is only called once per project load so it's safe to do this here. self.canvas.refresh() self.canvas.repaint() parser = roam.projectparser.ProjectParser.fromFile(self.project.projectfile) canvasnode = parser.canvasnode self.canvas.mapRenderer().readXML(canvasnode) self.canvaslayers = parser.canvaslayers() self.canvas.setLayerSet(self.canvaslayers) self.canvas.updateScale() self.canvas.refresh() self.mapisloaded = True def _saveproject(self): """ Save the project config to disk. """ self.write_config_currentwidget() # self.project.dump_settings() self.project.save(update_version=True) self.projectsaved.emit()