class SqlRelationalTableModel(QtSql.QSqlRelationalTableModel): def __init__(self, model, parent, db): super().__init__(parent, db) self.model = model self._proxyModel = QSortFilterProxyModel(self) self._proxyModel.setSortLocaleAware(True) self._proxyModel.setSourceModel(self) def relationModel(self, _column): return self.model def data(self, index, role=Qt.DisplayRole): if role == Qt.DecorationRole: if index.row() < 0: return None iconIndex = self.index(index.row(), self.fieldIndex('icon')) if not self.data(iconIndex) or self.data(iconIndex).isNull(): return None icon = QPixmap() icon.loadFromData(self.data(iconIndex)) return icon return super().data(index, role) def proxyModel(self): return self._proxyModel def sort(self, sort=True): if sort: self._proxyModel.sort(self.fieldIndex('value')) else: self._proxyModel.sort(-1)
class SqlRelationalTableModel(QtSql.QSqlRelationalTableModel): def __init__(self, model, parent, db): super().__init__(parent, db) self.model = model self._proxyModel = QSortFilterProxyModel(self) self._proxyModel.setSortLocaleAware(True) self._proxyModel.setSourceModel(self) def relationModel(self, _column): return self.model def data(self, index, role=Qt.DisplayRole): if role == Qt.DecorationRole: if index.row() < 0: return None iconIndex = self.index(index.row(), self.fieldIndex('icon')) if not self.data(iconIndex) or self.data(iconIndex).isNull(): return None icon = QPixmap() icon.loadFromData(self.data(iconIndex)) return icon return super().data(index, role) def proxyModel(self): return self._proxyModel def sort(self, sort=True): if sort: self._proxyModel.sort(self.fieldIndex('value')) else: self._proxyModel.sort(-1)
class TileStampsDock(QDockWidget): setStamp = pyqtSignal(TileStamp) def __init__(self, stampManager, parent=None): super().__init__(parent) self.mTileStampManager = stampManager self.mTileStampModel = stampManager.tileStampModel() self.mProxyModel = QSortFilterProxyModel(self.mTileStampModel) self.mFilterEdit = QLineEdit(self) self.mNewStamp = QAction(self) self.mAddVariation = QAction(self) self.mDuplicate = QAction(self) self.mDelete = QAction(self) self.mChooseFolder = QAction(self) self.setObjectName("TileStampsDock") self.mProxyModel.setSortLocaleAware(True) self.mProxyModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setSourceModel(self.mTileStampModel) self.mProxyModel.sort(0) self.mTileStampView = TileStampView(self) self.mTileStampView.setModel(self.mProxyModel) self.mTileStampView.setVerticalScrollMode( QAbstractItemView.ScrollPerPixel) self.mTileStampView.header().setStretchLastSection(False) self.mTileStampView.header().setSectionResizeMode( 0, QHeaderView.Stretch) self.mTileStampView.header().setSectionResizeMode( 1, QHeaderView.ResizeToContents) self.mTileStampView.setContextMenuPolicy(Qt.CustomContextMenu) self.mTileStampView.customContextMenuRequested.connect( self.showContextMenu) self.mNewStamp.setIcon(QIcon(":images/16x16/document-new.png")) self.mAddVariation.setIcon(QIcon(":/images/16x16/add.png")) self.mDuplicate.setIcon(QIcon(":/images/16x16/stock-duplicate-16.png")) self.mDelete.setIcon(QIcon(":images/16x16/edit-delete.png")) self.mChooseFolder.setIcon(QIcon(":images/16x16/document-open.png")) Utils.setThemeIcon(self.mNewStamp, "document-new") Utils.setThemeIcon(self.mAddVariation, "add") Utils.setThemeIcon(self.mDelete, "edit-delete") Utils.setThemeIcon(self.mChooseFolder, "document-open") self.mFilterEdit.setClearButtonEnabled(True) self.mFilterEdit.textChanged.connect( self.mProxyModel.setFilterFixedString) self.mTileStampModel.stampRenamed.connect(self.ensureStampVisible) self.mNewStamp.triggered.connect(self.newStamp) self.mAddVariation.triggered.connect(self.addVariation) self.mDuplicate.triggered.connect(self.duplicate) self.mDelete.triggered.connect(self.delete_) self.mChooseFolder.triggered.connect(self.chooseFolder) self.mDuplicate.setEnabled(False) self.mDelete.setEnabled(False) self.mAddVariation.setEnabled(False) widget = QWidget(self) layout = QVBoxLayout(widget) layout.setContentsMargins(5, 5, 5, 5) buttonContainer = QToolBar() buttonContainer.setFloatable(False) buttonContainer.setMovable(False) buttonContainer.setIconSize(QSize(16, 16)) buttonContainer.addAction(self.mNewStamp) buttonContainer.addAction(self.mAddVariation) buttonContainer.addAction(self.mDuplicate) buttonContainer.addAction(self.mDelete) stretch = QWidget() stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) buttonContainer.addWidget(stretch) buttonContainer.addAction(self.mChooseFolder) listAndToolBar = QVBoxLayout() listAndToolBar.setSpacing(0) listAndToolBar.addWidget(self.mFilterEdit) listAndToolBar.addWidget(self.mTileStampView) listAndToolBar.addWidget(buttonContainer) layout.addLayout(listAndToolBar) selectionModel = self.mTileStampView.selectionModel() selectionModel.currentRowChanged.connect(self.currentRowChanged) self.setWidget(widget) self.retranslateUi() def changeEvent(self, e): super().changeEvent(e) x = e.type() if x == QEvent.LanguageChange: self.retranslateUi() else: pass def keyPressEvent(self, event): x = event.key() if x == Qt.Key_Delete or x == Qt.Key_Backspace: self.delete_() return super().keyPressEvent(event) def currentRowChanged(self, index): sourceIndex = self.mProxyModel.mapToSource(index) isStamp = self.mTileStampModel.isStamp(sourceIndex) self.mDuplicate.setEnabled(isStamp) self.mDelete.setEnabled(sourceIndex.isValid()) self.mAddVariation.setEnabled(isStamp) if (isStamp): self.setStamp.emit(self.mTileStampModel.stampAt(sourceIndex)) else: variation = self.mTileStampModel.variationAt(sourceIndex) if variation: # single variation clicked, use it specifically self.setStamp.emit(TileStamp(Map(variation.map))) def showContextMenu(self, pos): index = self.mTileStampView.indexAt(pos) if (not index.isValid()): return menu = QMenu() sourceIndex = self.mProxyModel.mapToSource(index) if (self.mTileStampModel.isStamp(sourceIndex)): addStampVariation = QAction(self.mAddVariation.icon(), self.mAddVariation.text(), menu) deleteStamp = QAction(self.mDelete.icon(), self.tr("Delete Stamp"), menu) deleteStamp.triggered.connect(self.delete_) addStampVariation.triggered.connect(self.addVariation) menu.addAction(addStampVariation) menu.addSeparator() menu.addAction(deleteStamp) else: removeVariation = QAction(QIcon(":/images/16x16/remove.png"), self.tr("Remove Variation"), menu) Utils.setThemeIcon(removeVariation, "remove") removeVariation.triggered.connect(self.delete_) menu.addAction(removeVariation) menu.exec(self.mTileStampView.viewport().mapToGlobal(pos)) def newStamp(self): stamp = self.mTileStampManager.createStamp() if (self.isVisible() and not stamp.isEmpty()): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): viewIndex = self.mProxyModel.mapFromSource(stampIndex) self.mTileStampView.setCurrentIndex(viewIndex) self.mTileStampView.edit(viewIndex) def delete_(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) self.mTileStampModel.removeRow(sourceIndex.row(), sourceIndex.parent()) def duplicate(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt = TileStamp(sourceIndex) self.mTileStampModel.addStamp(stamp.clone()) def addVariation(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt(sourceIndex) self.mTileStampManager.addVariation(stamp) def chooseFolder(self): prefs = Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDirectory = QFileDialog.getExistingDirectory( self.window(), self.tr("Choose the Stamps Folder"), stampsDirectory) if (not stampsDirectory.isEmpty()): prefs.setStampsDirectory(stampsDirectory) def ensureStampVisible(self, stamp): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): self.mTileStampView.scrollTo( self.mProxyModel.mapFromSource(stampIndex)) def retranslateUi(self): self.setWindowTitle(self.tr("Tile Stamps")) self.mNewStamp.setText(self.tr("Add New Stamp")) self.mAddVariation.setText(self.tr("Add Variation")) self.mDuplicate.setText(self.tr("Duplicate Stamp")) self.mDelete.setText(self.tr("Delete Selected")) self.mChooseFolder.setText(self.tr("Set Stamps Folder")) self.mFilterEdit.setPlaceholderText(self.tr("Filter"))
class EmojisModel(): def update_model(self, clear=True): log.info("updating emoji model.") app = get_app() _ = app._tr # Clear all items if clear: self.model_paths = {} self.model.clear() self.emoji_groups.clear() # Add Headers self.model.setHorizontalHeaderLabels([_("Name")]) # Get emoji metadata emoji_metadata_path = os.path.join(info.PATH, "emojis", "data", "openmoji-optimized.json") with open(emoji_metadata_path, 'r', encoding="utf-8") as f: emoji_lookup = json.load(f) # get a list of files in the OpenShot /emojis directory emojis_dir = os.path.join(info.PATH, "emojis", "color", "svg") emoji_paths = [{"type": "common", "dir": emojis_dir, "files": os.listdir(emojis_dir)}, ] # Add optional user-defined transitions folder if os.path.exists(info.EMOJIS_PATH) and os.listdir(info.EMOJIS_PATH): emoji_paths.append({"type": "user", "dir": info.EMOJIS_PATH, "files": os.listdir(info.EMOJIS_PATH)}) for group in emoji_paths: dir = group["dir"] files = group["files"] for filename in sorted(files): path = os.path.join(dir, filename) fileBaseName = os.path.splitext(filename)[0] # Skip hidden files (such as .DS_Store, etc...) if filename[0] == "." or "thumbs.db" in filename.lower(): continue # get name of transition emoji = emoji_lookup.get(fileBaseName, {}) emoji_name = _(emoji.get("annotation", fileBaseName).capitalize()) emoji_type = _(emoji.get("group", "user").split('-')[0].capitalize()) # Track unique emoji groups if emoji_type not in self.emoji_groups: self.emoji_groups.append(emoji_type) # Check for thumbnail path (in build-in cache) thumb_path = os.path.join(info.IMAGES_PATH, "cache", "{}.png".format(fileBaseName)) # Check built-in cache (if not found) if not os.path.exists(thumb_path): # Check user folder cache thumb_path = os.path.join(info.CACHE_PATH, "{}.png".format(fileBaseName)) # Generate thumbnail (if needed) if not os.path.exists(thumb_path): try: # Reload this reader clip = openshot.Clip(path) reader = clip.Reader() # Open reader reader.Open() # Save thumbnail reader.GetFrame(0).Thumbnail( thumb_path, 75, 75, os.path.join(info.IMAGES_PATH, "mask.png"), "", "#000", True, "png", 85 ) reader.Close() clip.Close() except Exception: # Handle exception log.info('Invalid emoji image file: %s' % filename) msg = QMessageBox() msg.setText(_("{} is not a valid image file.".format(filename))) msg.exec_() continue row = [] # Set emoji data col = QStandardItem("Name") col.setIcon(QIcon(thumb_path)) col.setText(emoji_name) col.setToolTip(emoji_name) col.setData(path) col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsDragEnabled) row.append(col) # Append filterable group col = QStandardItem(emoji_type) row.append(col) # Append ROW to MODEL (if does not already exist in model) if path not in self.model_paths: self.model.appendRow(row) self.model_paths[path] = path def __init__(self, *args): # Create standard model self.app = get_app() self.model = EmojiStandardItemModel() self.model.setColumnCount(2) self.model_paths = {} self.emoji_groups = [] # Create proxy models (for grouping, sorting and filtering) self.group_model = QSortFilterProxyModel() self.group_model.setDynamicSortFilter(False) self.group_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.group_model.setSortCaseSensitivity(Qt.CaseSensitive) self.group_model.setSourceModel(self.model) self.group_model.setSortLocaleAware(True) self.group_model.setFilterKeyColumn(1) self.proxy_model = QSortFilterProxyModel() self.proxy_model.setDynamicSortFilter(False) self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.proxy_model.setSortCaseSensitivity(Qt.CaseSensitive) self.proxy_model.setSourceModel(self.group_model) self.proxy_model.setSortLocaleAware(True) # Attempt to load model testing interface, if requested # (will only succeed with Qt 5.11+) if info.MODEL_TEST: try: # Create model tester objects from PyQt5.QtTest import QAbstractItemModelTester self.model_tests = [] for m in [self.proxy_model, self.group_model, self.model]: self.model_tests.append( QAbstractItemModelTester( m, QAbstractItemModelTester.FailureReportingMode.Warning) ) log.info("Enabled {} model tests for emoji data".format(len(self.model_tests))) except ImportError: pass
class ListWidget(QWidget): deviceSelected = pyqtSignal(TasmotaDevice) openRulesEditor = pyqtSignal() openConsole = pyqtSignal() openTelemetry = pyqtSignal() openWebUI = pyqtSignal() def __init__(self, parent, *args, **kwargs): super(ListWidget, self).__init__(*args, **kwargs) self.setWindowTitle("Devices list") self.setWindowState(Qt.WindowMaximized) self.setLayout(VLayout(margin=0, spacing=0)) self.mqtt = parent.mqtt self.env = parent.env self.device = None self.idx = None self.nam = QNetworkAccessManager() self.backup = bytes() self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) views_order = self.settings.value("views_order", []) self.views = {} self.settings.beginGroup("Views") views = self.settings.childKeys() if views and views_order: for view in views_order.split(";"): view_list = self.settings.value(view).split(";") self.views[view] = base_view + view_list else: self.views = default_views self.settings.endGroup() self.tb = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.tb_relays = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonIconOnly) # self.tb_filter = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.tb_views = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.pwm_sliders = [] self.layout().addWidget(self.tb) self.layout().addWidget(self.tb_relays) # self.layout().addWidget(self.tb_filter) self.device_list = TableView() self.device_list.setIconSize(QSize(24, 24)) self.model = parent.device_model self.model.setupColumns(self.views["Home"]) self.sorted_device_model = QSortFilterProxyModel() self.sorted_device_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.sorted_device_model.setSourceModel(parent.device_model) self.sorted_device_model.setSortRole(Qt.InitialSortOrderRole) self.sorted_device_model.setSortLocaleAware(True) self.sorted_device_model.setFilterKeyColumn(-1) self.device_list.setModel(self.sorted_device_model) self.device_list.setupView(self.views["Home"]) self.device_list.setSortingEnabled(True) self.device_list.setWordWrap(True) self.device_list.setItemDelegate(DeviceDelegate()) self.device_list.sortByColumn(self.model.columnIndex("FriendlyName"), Qt.AscendingOrder) self.device_list.setContextMenuPolicy(Qt.CustomContextMenu) self.device_list.verticalHeader().setSectionResizeMode( QHeaderView.ResizeToContents) self.layout().addWidget(self.device_list) self.layout().addWidget(self.tb_views) self.device_list.clicked.connect(self.select_device) self.device_list.customContextMenuRequested.connect( self.show_list_ctx_menu) self.ctx_menu = QMenu() self.create_actions() self.create_view_buttons() # self.create_view_filter() self.device_list.doubleClicked.connect(lambda: self.openConsole.emit()) def create_actions(self): actConsole = self.tb.addAction(QIcon(":/console.png"), "Console", self.openConsole.emit) actConsole.setShortcut("Ctrl+E") actRules = self.tb.addAction(QIcon(":/rules.png"), "Rules", self.openRulesEditor.emit) actRules.setShortcut("Ctrl+R") actTimers = self.tb.addAction(QIcon(":/timers.png"), "Timers", self.configureTimers) actButtons = self.tb.addAction(QIcon(":/buttons.png"), "Buttons", self.configureButtons) actButtons.setShortcut("Ctrl+B") actSwitches = self.tb.addAction(QIcon(":/switches.png"), "Switches", self.configureSwitches) actSwitches.setShortcut("Ctrl+S") actPower = self.tb.addAction(QIcon(":/power.png"), "Power", self.configurePower) actPower.setShortcut("Ctrl+P") # setopts = self.tb.addAction(QIcon(":/setoptions.png"), "SetOptions", self.configureSO) # setopts.setShortcut("Ctrl+S") self.tb.addSpacer() actTelemetry = self.tb.addAction(QIcon(":/telemetry.png"), "Telemetry", self.openTelemetry.emit) actTelemetry.setShortcut("Ctrl+T") actWebui = self.tb.addAction(QIcon(":/web.png"), "WebUI", self.openWebUI.emit) actWebui.setShortcut("Ctrl+U") self.ctx_menu.addActions([ actRules, actTimers, actButtons, actSwitches, actPower, actTelemetry, actWebui ]) self.ctx_menu.addSeparator() self.ctx_menu_cfg = QMenu("Configure") self.ctx_menu_cfg.setIcon(QIcon(":/settings.png")) self.ctx_menu_cfg.addAction("Module", self.configureModule) self.ctx_menu_cfg.addAction("GPIO", self.configureGPIO) self.ctx_menu_cfg.addAction("Template", self.configureTemplate) # self.ctx_menu_cfg.addAction("Wifi", self.ctx_menu_teleperiod) # self.ctx_menu_cfg.addAction("Time", self.cfgTime.emit) # self.ctx_menu_cfg.addAction("MQTT", self.ctx_menu_teleperiod) # self.ctx_menu_cfg.addAction("Logging", self.ctx_menu_teleperiod) self.ctx_menu.addMenu(self.ctx_menu_cfg) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon(":/refresh.png"), "Refresh", self.ctx_menu_refresh) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon(":/clear.png"), "Clear retained", self.ctx_menu_clear_retained) self.ctx_menu.addAction("Clear Backlog", self.ctx_menu_clear_backlog) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon(":/copy.png"), "Copy", self.ctx_menu_copy) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon(":/restart.png"), "Restart", self.ctx_menu_restart) self.ctx_menu.addAction(QIcon(), "Reset", self.ctx_menu_reset) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon(":/delete.png"), "Delete", self.ctx_menu_delete_device) # self.tb.addAction(QIcon(), "Multi Command", self.ctx_menu_webui) self.agAllPower = QActionGroup(self) self.agAllPower.addAction(QIcon(":/P_ON.png"), "All ON") self.agAllPower.addAction(QIcon(":/P_OFF.png"), "All OFF") self.agAllPower.setEnabled(False) self.agAllPower.setExclusive(False) self.agAllPower.triggered.connect(self.toggle_power_all) self.tb_relays.addActions(self.agAllPower.actions()) self.agRelays = QActionGroup(self) self.agRelays.setVisible(False) self.agRelays.setExclusive(False) for a in range(1, 9): act = QAction(QIcon(":/P{}_OFF.png".format(a)), "") act.setShortcut("F{}".format(a)) self.agRelays.addAction(act) self.agRelays.triggered.connect(self.toggle_power) self.tb_relays.addActions(self.agRelays.actions()) self.tb_relays.addSeparator() self.actColor = self.tb_relays.addAction(QIcon(":/color.png"), "Color", self.set_color) self.actColor.setEnabled(False) self.actChannels = self.tb_relays.addAction(QIcon(":/sliders.png"), "Channels") self.actChannels.setEnabled(False) self.mChannels = QMenu() self.actChannels.setMenu(self.mChannels) self.tb_relays.widgetForAction(self.actChannels).setPopupMode( QToolButton.InstantPopup) def create_view_buttons(self): self.tb_views.addWidget(QLabel("View mode: ")) ag_views = QActionGroup(self) ag_views.setExclusive(True) for v in self.views.keys(): a = QAction(v) a.triggered.connect(self.change_view) a.setCheckable(True) ag_views.addAction(a) self.tb_views.addActions(ag_views.actions()) ag_views.actions()[0].setChecked(True) stretch = QWidget() stretch.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)) self.tb_views.addWidget(stretch) # actEditView = self.tb_views.addAction("Edit views...") # def create_view_filter(self): # # self.tb_filter.addWidget(QLabel("Show devices: ")) # # self.cbxLWT = QComboBox() # # self.cbxLWT.addItems(["All", "Online"d, "Offline"]) # # self.cbxLWT.currentTextChanged.connect(self.build_filter_regex) # # self.tb_filter.addWidget(self.cbxLWT) # # self.tb_filter.addWidget(QLabel(" Search: ")) # self.leSearch = QLineEdit() # self.leSearch.setClearButtonEnabled(True) # self.leSearch.textChanged.connect(self.build_filter_regex) # self.tb_filter.addWidget(self.leSearch) # # def build_filter_regex(self, txt): # query = self.leSearch.text() # # if self.cbxLWT.currentText() != "All": # # query = "{}|{}".format(self.cbxLWT.currentText(), query) # self.sorted_device_model.setFilterRegExp(query) def change_view(self, a=None): view = self.views[self.sender().text()] self.model.setupColumns(view) self.device_list.setupView(view) def ctx_menu_copy(self): if self.idx: string = dumps(self.model.data(self.idx)) if string.startswith('"') and string.endswith('"'): string = string[1:-1] QApplication.clipboard().setText(string) def ctx_menu_clear_retained(self): if self.device: relays = self.device.power() if relays and len(relays.keys()) > 0: for r in relays.keys(): self.mqtt.publish(self.device.cmnd_topic(r), retain=True) QMessageBox.information(self, "Clear retained", "Cleared retained messages.") def ctx_menu_clear_backlog(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("backlog"), "") QMessageBox.information(self, "Clear Backlog", "Backlog cleared.") def ctx_menu_restart(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("restart"), payload="1") for k in list(self.device.power().keys()): self.device.p.pop(k) def ctx_menu_reset(self): if self.device: reset, ok = QInputDialog.getItem(self, "Reset device and restart", "Select reset mode", resets, editable=False) if ok: self.mqtt.publish(self.device.cmnd_topic("reset"), payload=reset.split(":")[0]) for k in list(self.device.power().keys()): self.device.p.pop(k) def ctx_menu_refresh(self): if self.device: for k in list(self.device.power().keys()): self.device.p.pop(k) for c in initial_commands(): cmd, payload = c cmd = self.device.cmnd_topic(cmd) self.mqtt.publish(cmd, payload, 1) def ctx_menu_delete_device(self): if self.device: if QMessageBox.question( self, "Confirm", "Do you want to remove the following device?\n'{}' ({})". format(self.device.p['FriendlyName1'], self.device.p['Topic'])) == QMessageBox.Yes: self.model.deleteDevice(self.idx) def ctx_menu_teleperiod(self): if self.device: teleperiod, ok = QInputDialog.getInt( self, "Set telemetry period", "Input 1 to reset to default\n[Min: 10, Max: 3600]", self.device.p['TelePeriod'], 1, 3600) if ok: if teleperiod != 1 and teleperiod < 10: teleperiod = 10 self.mqtt.publish(self.device.cmnd_topic("teleperiod"), teleperiod) def ctx_menu_config_backup(self): if self.device: self.backup = bytes() self.dl = self.nam.get( QNetworkRequest( QUrl("http://{}/dl".format(self.device.p['IPAddress'])))) self.dl.readyRead.connect(self.get_dump) self.dl.finished.connect(self.save_dump) def ctx_menu_ota_set_url(self): if self.device: url, ok = QInputDialog.getText( self, "Set OTA URL", '100 chars max. Set to "1" to reset to default.', text=self.device.p['OtaUrl']) if ok: self.mqtt.publish(self.device.cmnd_topic("otaurl"), payload=url) def ctx_menu_ota_set_upgrade(self): if self.device: if QMessageBox.question( self, "OTA Upgrade", "Are you sure to OTA upgrade from\n{}".format( self.device.p['OtaUrl']), QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: self.mqtt.publish(self.device.cmnd_topic("upgrade"), payload="1") def show_list_ctx_menu(self, at): self.select_device(self.device_list.indexAt(at)) self.ctx_menu.popup(self.device_list.viewport().mapToGlobal(at)) def select_device(self, idx): self.idx = self.sorted_device_model.mapToSource(idx) self.device = self.model.deviceAtRow(self.idx.row()) self.deviceSelected.emit(self.device) relays = self.device.power() self.agAllPower.setEnabled(len(relays) >= 1) for i, a in enumerate(self.agRelays.actions()): a.setVisible(len(relays) > 1 and i < len(relays)) color = self.device.color().get("Color", False) has_color = bool(color) self.actColor.setEnabled(has_color and not self.device.setoption(68)) self.actChannels.setEnabled(has_color) if has_color: self.actChannels.menu().clear() max_val = 100 if self.device.setoption(15) == 0: max_val = 1023 for k, v in self.device.pwm().items(): channel = SliderAction(self, k) channel.slider.setMaximum(max_val) channel.slider.setValue(int(v)) self.mChannels.addAction(channel) channel.slider.valueChanged.connect(self.set_channel) dimmer = self.device.color().get("Dimmer") if dimmer: saDimmer = SliderAction(self, "Dimmer") saDimmer.slider.setValue(int(dimmer)) self.mChannels.addAction(saDimmer) saDimmer.slider.valueChanged.connect(self.set_channel) def toggle_power(self, action): if self.device: idx = self.agRelays.actions().index(action) relay = sorted(list(self.device.power().keys()))[idx] self.mqtt.publish(self.device.cmnd_topic(relay), "toggle") def toggle_power_all(self, action): if self.device: idx = self.agAllPower.actions().index(action) for r in sorted(self.device.power().keys()): self.mqtt.publish(self.device.cmnd_topic(r), idx ^ 1) def set_color(self): if self.device: color = self.device.color().get("Color") if color: dlg = QColorDialog() new_color = dlg.getColor(QColor("#{}".format(color))) if new_color.isValid(): new_color = new_color.name() if new_color != color: self.mqtt.publish(self.device.cmnd_topic("color"), new_color) def set_channel(self, value=0): cmd = self.sender().objectName() if self.device: self.mqtt.publish(self.device.cmnd_topic(cmd), str(value)) def configureSO(self): if self.device: dlg = SetOptionsDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureModule(self): if self.device: dlg = ModuleDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureGPIO(self): if self.device: dlg = GPIODialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureTemplate(self): if self.device: dlg = TemplateDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureTimers(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("timers")) timers = TimersDialog(self.device) self.mqtt.messageSignal.connect(timers.parseMessage) timers.sendCommand.connect(self.mqtt.publish) timers.exec_() def configureButtons(self): if self.device: backlog = [] buttons = ButtonsDialog(self.device) if buttons.exec_() == QDialog.Accepted: for c, cw in buttons.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) so_error = False for so, sow in buttons.setoption_widgets.items(): current_value = None try: current_value = self.device.setoption(so) except ValueError: so_error = True new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if not so_error and current_value and current_value != new_value: backlog.append("SetOption{} {}".format(so, new_value)) if backlog: backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def configureSwitches(self): if self.device: backlog = [] switches = SwitchesDialog(self.device) if switches.exec_() == QDialog.Accepted: for c, cw in switches.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) so_error = False for so, sow in switches.setoption_widgets.items(): current_value = None try: current_value = self.device.setoption(so) except ValueError: so_error = True new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if not so_error and current_value != new_value: backlog.append("SetOption{} {}".format(so, new_value)) for sw, sw_mode in enumerate(self.device.p['SwitchMode']): new_value = switches.sm.inputs[sw].currentIndex() if sw_mode != new_value: backlog.append("switchmode{} {}".format( sw + 1, new_value)) if backlog: backlog.append("status") backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def configurePower(self): if self.device: backlog = [] power = PowerDialog(self.device) if power.exec_() == QDialog.Accepted: for c, cw in power.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) so_error = False for so, sow in power.setoption_widgets.items(): current_value = None try: current_value = self.device.setoption(so) except ValueError: so_error = True new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if not so_error and current_value != new_value: backlog.append("SetOption{} {}".format(so, new_value)) new_interlock_value = power.ci.input.currentData() new_interlock_grps = " ".join([ grp.text().replace(" ", "") for grp in power.ci.groups ]).rstrip() if new_interlock_value != self.device.p.get( "Interlock", "OFF"): backlog.append("interlock {}".format(new_interlock_value)) if new_interlock_grps != self.device.p.get("Groups", ""): backlog.append("interlock {}".format(new_interlock_grps)) for i, pt in enumerate(power.cpt.inputs): ptime = "PulseTime{}".format(i + 1) current_ptime = self.device.p.get(ptime) if current_ptime: current_value = list(current_ptime.keys())[0] new_value = str(pt.value()) if new_value != current_value: backlog.append("{} {}".format(ptime, new_value)) if backlog: backlog.append("status") backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def get_dump(self): self.backup += self.dl.readAll() def save_dump(self): fname = self.dl.header(QNetworkRequest.ContentDispositionHeader) if fname: fname = fname.split('=')[1] save_file = QFileDialog.getSaveFileName( self, "Save config backup", "{}/TDM/{}".format(QDir.homePath(), fname))[0] if save_file: with open(save_file, "wb") as f: f.write(self.backup) def check_fulltopic(self, fulltopic): fulltopic += "/" if not fulltopic.endswith('/') else '' return "%prefix%" in fulltopic and "%topic%" in fulltopic def closeEvent(self, event): event.ignore()
class TileStampsDock(QDockWidget): setStamp = pyqtSignal(TileStamp) def __init__(self, stampManager, parent = None): super().__init__(parent) self.mTileStampManager = stampManager self.mTileStampModel = stampManager.tileStampModel() self.mProxyModel = QSortFilterProxyModel(self.mTileStampModel) self.mFilterEdit = QLineEdit(self) self.mNewStamp = QAction(self) self.mAddVariation = QAction(self) self.mDuplicate = QAction(self) self.mDelete = QAction(self) self.mChooseFolder = QAction(self) self.setObjectName("TileStampsDock") self.mProxyModel.setSortLocaleAware(True) self.mProxyModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setSourceModel(self.mTileStampModel) self.mProxyModel.sort(0) self.mTileStampView = TileStampView(self) self.mTileStampView.setModel(self.mProxyModel) self.mTileStampView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.mTileStampView.header().setStretchLastSection(False) self.mTileStampView.header().setSectionResizeMode(0, QHeaderView.Stretch) self.mTileStampView.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.mTileStampView.setContextMenuPolicy(Qt.CustomContextMenu) self.mTileStampView.customContextMenuRequested.connect(self.showContextMenu) self.mNewStamp.setIcon(QIcon(":images/16x16/document-new.png")) self.mAddVariation.setIcon(QIcon(":/images/16x16/add.png")) self.mDuplicate.setIcon(QIcon(":/images/16x16/stock-duplicate-16.png")) self.mDelete.setIcon(QIcon(":images/16x16/edit-delete.png")) self.mChooseFolder.setIcon(QIcon(":images/16x16/document-open.png")) Utils.setThemeIcon(self.mNewStamp, "document-new") Utils.setThemeIcon(self.mAddVariation, "add") Utils.setThemeIcon(self.mDelete, "edit-delete") Utils.setThemeIcon(self.mChooseFolder, "document-open") self.mFilterEdit.setClearButtonEnabled(True) self.mFilterEdit.textChanged.connect(self.mProxyModel.setFilterFixedString) self.mTileStampModel.stampRenamed.connect(self.ensureStampVisible) self.mNewStamp.triggered.connect(self.newStamp) self.mAddVariation.triggered.connect(self.addVariation) self.mDuplicate.triggered.connect(self.duplicate) self.mDelete.triggered.connect(self.delete_) self.mChooseFolder.triggered.connect(self.chooseFolder) self.mDuplicate.setEnabled(False) self.mDelete.setEnabled(False) self.mAddVariation.setEnabled(False) widget = QWidget(self) layout = QVBoxLayout(widget) layout.setContentsMargins(5, 5, 5, 5) buttonContainer = QToolBar() buttonContainer.setFloatable(False) buttonContainer.setMovable(False) buttonContainer.setIconSize(QSize(16, 16)) buttonContainer.addAction(self.mNewStamp) buttonContainer.addAction(self.mAddVariation) buttonContainer.addAction(self.mDuplicate) buttonContainer.addAction(self.mDelete) stretch = QWidget() stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) buttonContainer.addWidget(stretch) buttonContainer.addAction(self.mChooseFolder) listAndToolBar = QVBoxLayout() listAndToolBar.setSpacing(0) listAndToolBar.addWidget(self.mFilterEdit) listAndToolBar.addWidget(self.mTileStampView) listAndToolBar.addWidget(buttonContainer) layout.addLayout(listAndToolBar) selectionModel = self.mTileStampView.selectionModel() selectionModel.currentRowChanged.connect(self.currentRowChanged) self.setWidget(widget) self.retranslateUi() def changeEvent(self, e): super().changeEvent(e) x = e.type() if x==QEvent.LanguageChange: self.retranslateUi() else: pass def keyPressEvent(self, event): x = event.key() if x==Qt.Key_Delete or x==Qt.Key_Backspace: self.delete_() return super().keyPressEvent(event) def currentRowChanged(self, index): sourceIndex = self.mProxyModel.mapToSource(index) isStamp = self.mTileStampModel.isStamp(sourceIndex) self.mDuplicate.setEnabled(isStamp) self.mDelete.setEnabled(sourceIndex.isValid()) self.mAddVariation.setEnabled(isStamp) if (isStamp): self.setStamp.emit(self.mTileStampModel.stampAt(sourceIndex)) else: variation = self.mTileStampModel.variationAt(sourceIndex) if variation: # single variation clicked, use it specifically self.setStamp.emit(TileStamp(Map(variation.map))) def showContextMenu(self, pos): index = self.mTileStampView.indexAt(pos) if (not index.isValid()): return menu = QMenu() sourceIndex = self.mProxyModel.mapToSource(index) if (self.mTileStampModel.isStamp(sourceIndex)): addStampVariation = QAction(self.mAddVariation.icon(), self.mAddVariation.text(), menu) deleteStamp = QAction(self.mDelete.icon(), self.tr("Delete Stamp"), menu) deleteStamp.triggered.connect(self.delete_) addStampVariation.triggered.connect(self.addVariation) menu.addAction(addStampVariation) menu.addSeparator() menu.addAction(deleteStamp) else : removeVariation = QAction(QIcon(":/images/16x16/remove.png"), self.tr("Remove Variation"), menu) Utils.setThemeIcon(removeVariation, "remove") removeVariation.triggered.connect(self.delete_) menu.addAction(removeVariation) menu.exec(self.mTileStampView.viewport().mapToGlobal(pos)) def newStamp(self): stamp = self.mTileStampManager.createStamp() if (self.isVisible() and not stamp.isEmpty()): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): viewIndex = self.mProxyModel.mapFromSource(stampIndex) self.mTileStampView.setCurrentIndex(viewIndex) self.mTileStampView.edit(viewIndex) def delete_(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) self.mTileStampModel.removeRow(sourceIndex.row(), sourceIndex.parent()) def duplicate(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt = TileStamp(sourceIndex) self.mTileStampModel.addStamp(stamp.clone()) def addVariation(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt(sourceIndex) self.mTileStampManager.addVariation(stamp) def chooseFolder(self): prefs = Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDirectory = QFileDialog.getExistingDirectory(self.window(), self.tr("Choose the Stamps Folder"), stampsDirectory) if (not stampsDirectory.isEmpty()): prefs.setStampsDirectory(stampsDirectory) def ensureStampVisible(self, stamp): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): self.mTileStampView.scrollTo(self.mProxyModel.mapFromSource(stampIndex)) def retranslateUi(self): self.setWindowTitle(self.tr("Tile Stamps")) self.mNewStamp.setText(self.tr("Add New Stamp")) self.mAddVariation.setText(self.tr("Add Variation")) self.mDuplicate.setText(self.tr("Duplicate Stamp")) self.mDelete.setText(self.tr("Delete Selected")) self.mChooseFolder.setText(self.tr("Set Stamps Folder")) self.mFilterEdit.setPlaceholderText(self.tr("Filter"))