class External(): def __init__(self): self.watchTimer = QTimer() self.watchTimer.setInterval(800) self.watchTimer.timerEvent = self.onWatch self.files_changed = queue.Queue() pass def onWatch(self,e): try: file_type, filename, action = self.files_changed.get_nowait() print(file_type, filename, action) except queue.Empty: pass pass def onFileChanged(self, path=None): print('file changed', path) pass def load(self, path): print('load', path) Watcher(path, self.files_changed) if not self.watchTimer.isActive(): self.watchTimer.start() pass pass def update(self, path): pass def create(self, tmppath=None): if not tmppath: tmppath = 'tmp' if not os.path.exists(tmppath): os.mkdir(tmppath) pass pass
class ScudCloud(QtWidgets.QMainWindow): forceClose = False messages = 0 speller = Speller() title = 'ScudCloud' def __init__(self, debug = False, minimized = None, urgent_hint = None, settings_path = '', cache_path = ''): super(ScudCloud, self).__init__(None) self.debug = debug self.minimized = minimized self.urgent_hint = urgent_hint self.setWindowTitle(self.title) self.settings_path = settings_path self.cache_path = cache_path self.notifier = Notifier(Resources.APP_NAME, Resources.get_path('scudcloud.png')) self.settings = QSettings(self.settings_path + '/scudcloud_qt5.cfg', QSettings.IniFormat) self.notifier.enabled = self.settings.value('Notifications', defaultValue=True, type=bool) self.identifier = self.settings.value("Domain") if Unity is not None: self.launcher = Unity.LauncherEntry.get_for_desktop_id("scudcloud.desktop") else: self.launcher = DummyLauncher(self) self.webSettings() self.snippetsSettings() self.leftPane = LeftPane(self) self.stackedWidget = QtWidgets.QStackedWidget() centralWidget = QtWidgets.QWidget(self) layout = QtWidgets.QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.leftPane) layout.addWidget(self.stackedWidget) centralWidget.setLayout(layout) self.setCentralWidget(centralWidget) self.startURL = Resources.SIGNIN_URL if self.identifier is not None: if isinstance(self.identifier, str): self.domains = self.identifier.split(",") else: self.domains = self.identifier self.startURL = self.normalize(self.domains[0]) else: self.domains = [] self.addWrapper(self.startURL) self.addMenu() self.tray = Systray(self) self.systray(self.minimized) self.installEventFilter(self) self.statusBar().showMessage('Loading Slack...') self.tickler = QTimer(self) self.tickler.setInterval(1800000) # Watch for ScreenLock events if DBusQtMainLoop is not None: DBusQtMainLoop(set_as_default=True) sessionBus = dbus.SessionBus() # Ubuntu 12.04 and other distros sessionBus.add_match_string("type='signal',interface='org.gnome.ScreenSaver'") # Ubuntu 14.04 sessionBus.add_match_string("type='signal',interface='com.ubuntu.Upstart0_6'") # Ubuntu 16.04 and KDE sessionBus.add_match_string("type='signal',interface='org.freedesktop.ScreenSaver'") # Cinnamon sessionBus.add_match_string("type='signal',interface='org.cinnamon.ScreenSaver'") sessionBus.add_message_filter(self.screenListener) self.tickler.timeout.connect(self.sendTickle) # If dbus is not present, tickler timer will act like a blocker to not send tickle too often else: self.tickler.setSingleShot(True) self.tickler.start() def screenListener(self, bus, message): event = message.get_member() # "ActiveChanged" for Ubuntu 12.04 and other distros. "EventEmitted" for Ubuntu 14.04 and above if event == "ActiveChanged" or event == "EventEmitted": arg = message.get_args_list()[0] # True for Ubuntu 12.04 and other distros. "desktop-lock" for Ubuntu 14.04 and above if (arg == True or arg == "desktop-lock") and self.tickler.isActive(): self.tickler.stop() elif (arg == False or arg == "desktop-unlock") and not self.tickler.isActive(): self.sendTickle() self.tickler.start() def sendTickle(self): for i in range(0, self.stackedWidget.count()): self.stackedWidget.widget(i).sendTickle() def addWrapper(self, url): webView = Wrapper(self) webView.load(QtCore.QUrl(url)) webView.show() webView.setZoomFactor(self.zoom) self.stackedWidget.addWidget(webView) self.stackedWidget.setCurrentWidget(webView) self.clearMemory() def webSettings(self): self.cookiesjar = PersistentCookieJar(self) self.zoom = self.readZoom() # We don't want Flash (it causes a lot of trouble in some distros) QWebSettings.globalSettings().setAttribute(QWebSettings.PluginsEnabled, False) # We don't need Java QWebSettings.globalSettings().setAttribute(QWebSettings.JavaEnabled, False) # Enabling Local Storage (now required by Slack) QWebSettings.globalSettings().setAttribute(QWebSettings.LocalStorageEnabled, True) # We need browsing history (required to not limit LocalStorage) QWebSettings.globalSettings().setAttribute(QWebSettings.PrivateBrowsingEnabled, False) # Enabling Cache self.diskCache = QNetworkDiskCache(self) self.diskCache.setCacheDirectory(self.cache_path) # Required for copy and paste clipboard integration QWebSettings.globalSettings().setAttribute(QWebSettings.JavascriptCanAccessClipboard, True) # Enabling Inspeclet only when --debug=True (requires more CPU usage) QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, self.debug) # Sharing the same networkAccessManager self.networkAccessManager = QNetworkAccessManager(self) self.networkAccessManager.setCookieJar(self.cookiesjar) self.networkAccessManager.setCache(self.diskCache) def snippetsSettings(self): self.disable_snippets = self.settings.value("Snippets") if self.disable_snippets is not None: self.disable_snippets = self.disable_snippets == "False" else: self.disable_snippets = False if self.disable_snippets: disable_snippets_css = '' with open(Resources.get_path('disable_snippets.css'), 'r') as f: disable_snippets_css = f.read() with open(os.path.join(self.cache_path, 'resources.css'), 'a') as f: f.write(disable_snippets_css) def toggleFullScreen(self): if self.isFullScreen(): self.showMaximized() else: self.showFullScreen() def toggleMenuBar(self): menu = self.menuBar() state = menu.isHidden() menu.setVisible(state) if state: self.settings.setValue("Menu", "True") else: self.settings.setValue("Menu", "False") def restore(self): geometry = self.settings.value("geometry") if geometry is not None: self.restoreGeometry(geometry) windowState = self.settings.value("windowState") if windowState is not None: self.restoreState(windowState) else: self.setWindowState(QtCore.Qt.WindowMaximized) def systray(self, show=None): if show is None: show = self.settings.value("Systray") == "True" if show: self.tray.show() self.menus["file"]["close"].setEnabled(True) self.settings.setValue("Systray", "True") else: self.tray.setVisible(False) self.menus["file"]["close"].setEnabled(False) self.settings.setValue("Systray", "False") def readZoom(self): default = 1 if self.settings.value("Zoom") is not None: default = float(self.settings.value("Zoom")) return default def setZoom(self, factor=1): if factor > 0: for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) widget.setZoomFactor(factor) self.settings.setValue("Zoom", factor) def zoomIn(self): self.setZoom(self.current().zoomFactor() + 0.1) def zoomOut(self): self.setZoom(self.current().zoomFactor() - 0.1) def zoomReset(self): self.setZoom() def addTeam(self): self.switchTo(Resources.SIGNIN_URL) def addMenu(self): # We'll register the webpage shorcuts with the window too (Fixes #338) undo = self.current().pageAction(QWebPage.Undo) redo = self.current().pageAction(QWebPage.Redo) cut = self.current().pageAction(QWebPage.Cut) copy = self.current().pageAction(QWebPage.Copy) paste = self.current().pageAction(QWebPage.Paste) back = self.current().pageAction(QWebPage.Back) forward = self.current().pageAction(QWebPage.Forward) reload = self.current().pageAction(QWebPage.Reload) self.menus = { "file": { "preferences": self.createAction("Preferences", lambda : self.current().preferences()), "systray": self.createAction("Close to Tray", self.systray, None, True), "addTeam": self.createAction("Sign in to Another Team", lambda : self.addTeam()), "signout": self.createAction("Signout", lambda : self.current().logout()), "close": self.createAction("Close", self.close, QKeySequence.Close), "exit": self.createAction("Quit", self.exit, QKeySequence.Quit) }, "edit": { "undo": self.createAction(undo.text(), lambda : self.current().page().triggerAction(QWebPage.Undo), undo.shortcut()), "redo": self.createAction(redo.text(), lambda : self.current().page().triggerAction(QWebPage.Redo), redo.shortcut()), "cut": self.createAction(cut.text(), lambda : self.current().page().triggerAction(QWebPage.Cut), cut.shortcut()), "copy": self.createAction(copy.text(), lambda : self.current().page().triggerAction(QWebPage.Copy), copy.shortcut()), "paste": self.createAction(paste.text(), lambda : self.current().page().triggerAction(QWebPage.Paste), paste.shortcut()), "back": self.createAction(back.text(), lambda : self.current().page().triggerAction(QWebPage.Back), back.shortcut()), "forward": self.createAction(forward.text(), lambda : self.current().page().triggerAction(QWebPage.Forward), forward.shortcut()), "reload": self.createAction(reload.text(), lambda : self.current().page().triggerAction(QWebPage.Reload), reload.shortcut()), }, "view": { "zoomin": self.createAction("Zoom In", self.zoomIn, QKeySequence.ZoomIn), "zoomout": self.createAction("Zoom Out", self.zoomOut, QKeySequence.ZoomOut), "reset": self.createAction("Reset", self.zoomReset, QtCore.Qt.CTRL + QtCore.Qt.Key_0), "fullscreen": self.createAction("Toggle Full Screen", self.toggleFullScreen, QtCore.Qt.Key_F11), "hidemenu": self.createAction("Toggle Menubar", self.toggleMenuBar, QtCore.Qt.Key_F12) }, "help": { "help": self.createAction("Help and Feedback", lambda : self.current().help(), QKeySequence.HelpContents), "center": self.createAction("Slack Help Center", lambda : self.current().helpCenter()), "about": self.createAction("About", lambda : self.current().about()) } } menu = self.menuBar() fileMenu = menu.addMenu("&File") fileMenu.addAction(self.menus["file"]["preferences"]) fileMenu.addAction(self.menus["file"]["systray"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["addTeam"]) fileMenu.addAction(self.menus["file"]["signout"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["close"]) fileMenu.addAction(self.menus["file"]["exit"]) editMenu = menu.addMenu("&Edit") editMenu.addAction(self.menus["edit"]["undo"]) editMenu.addAction(self.menus["edit"]["redo"]) editMenu.addSeparator() editMenu.addAction(self.menus["edit"]["cut"]) editMenu.addAction(self.menus["edit"]["copy"]) editMenu.addAction(self.menus["edit"]["paste"]) editMenu.addSeparator() editMenu.addAction(self.menus["edit"]["back"]) editMenu.addAction(self.menus["edit"]["forward"]) editMenu.addAction(self.menus["edit"]["reload"]) viewMenu = menu.addMenu("&View") viewMenu.addAction(self.menus["view"]["zoomin"]) viewMenu.addAction(self.menus["view"]["zoomout"]) viewMenu.addAction(self.menus["view"]["reset"]) viewMenu.addSeparator() viewMenu.addAction(self.menus["view"]["fullscreen"]) viewMenu.addAction(self.menus["view"]["hidemenu"]) helpMenu = menu.addMenu("&Help") helpMenu.addAction(self.menus["help"]["help"]) helpMenu.addAction(self.menus["help"]["center"]) helpMenu.addSeparator() helpMenu.addAction(self.menus["help"]["about"]) self.enableMenus(False) showSystray = self.settings.value("Systray") == "True" self.menus["file"]["systray"].setChecked(showSystray) self.menus["file"]["close"].setEnabled(showSystray) # Restore menu visibility visible = self.settings.value("Menu") if visible is not None and visible == "False": menu.setVisible(False) def enableMenus(self, enabled): self.menus["file"]["preferences"].setEnabled(enabled == True) self.menus["file"]["addTeam"].setEnabled(enabled == True) self.menus["file"]["signout"].setEnabled(enabled == True) self.menus["help"]["help"].setEnabled(enabled == True) def createAction(self, text, slot, shortcut=None, checkable=False): action = QtWidgets.QAction(text, self) action.triggered.connect(slot) if shortcut is not None: action.setShortcut(shortcut) self.addAction(action) if checkable: action.setCheckable(True) return action def normalize(self, url): if url.endswith(".slack.com"): url+= "/" elif not url.endswith(".slack.com/"): url = "https://"+url+".slack.com/" return url def current(self): return self.stackedWidget.currentWidget() def teams(self, teams): if len(self.domains) == 0: self.domains.append(teams[0]['team_url']) team_list = [t['team_url'] for t in teams] for t in teams: for i in range(0, len(self.domains)): self.domains[i] = self.normalize(self.domains[i]) # When team_icon is missing, the team already exists (Fixes #381, #391) if 'team_icon' in t: if self.domains[i] in team_list: add = next(item for item in teams if item['team_url'] == self.domains[i]) if 'team_icon' in add: self.leftPane.addTeam(add['id'], add['team_name'], add['team_url'], add['team_icon']['image_44'], add == teams[0]) # Adding new teams and saving loading positions if t['team_url'] not in self.domains: self.leftPane.addTeam(t['id'], t['team_name'], t['team_url'], t['team_icon']['image_44'], t == teams[0]) self.domains.append(t['team_url']) self.settings.setValue("Domain", self.domains) if len(teams) > 1: self.leftPane.show() def switchTo(self, url): exists = False for i in range(0, self.stackedWidget.count()): if self.stackedWidget.widget(i).url().toString().startswith(url): self.stackedWidget.setCurrentIndex(i) self.quicklist(self.current().listChannels()) self.current().setFocus() self.leftPane.click(i) self.clearMemory() exists = True break if not exists: self.addWrapper(url) def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.ActivationChange and self.isActiveWindow(): self.focusInEvent(event) if event.type() == QtCore.QEvent.KeyPress: # Ctrl + <n> modifiers = QtWidgets.QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ControlModifier: if event.key() == QtCore.Qt.Key_1: self.leftPane.click(0) elif event.key() == QtCore.Qt.Key_2: self.leftPane.click(1) elif event.key() == QtCore.Qt.Key_3: self.leftPane.click(2) elif event.key() == QtCore.Qt.Key_4: self.leftPane.click(3) elif event.key() == QtCore.Qt.Key_5: self.leftPane.click(4) elif event.key() == QtCore.Qt.Key_6: self.leftPane.click(5) elif event.key() == QtCore.Qt.Key_7: self.leftPane.click(6) elif event.key() == QtCore.Qt.Key_8: self.leftPane.click(7) elif event.key() == QtCore.Qt.Key_9: self.leftPane.click(8) # Ctrl + Tab elif event.key() == QtCore.Qt.Key_Tab: self.leftPane.clickNext(1) # Ctrl + BackTab if (modifiers & QtCore.Qt.ControlModifier) and (modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_Backtab: self.leftPane.clickNext(-1) # Ctrl + Shift + <key> if (modifiers & QtCore.Qt.ShiftModifier) and (modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_V: self.current().createSnippet() return QtWidgets.QMainWindow.eventFilter(self, obj, event); def focusInEvent(self, event): self.launcher.set_property("urgent", False) self.tray.stopAlert() # Let's tickle all teams on window focus, but only if tickle was not fired in last 30 minutes if DBusQtMainLoop is None and not self.tickler.isActive(): self.sendTickle() self.tickler.start() def titleChanged(self): self.setWindowTitle(self.current().title()) def setForceClose(self): self.forceClose = True def closeEvent(self, event): if not self.forceClose and self.settings.value("Systray") == "True": self.hide() event.ignore() elif self.forceClose: self.cookiesjar.save() self.settings.setValue("Domain", self.domains) self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) self.settings.setValue("Domain", self.domains) def show(self): self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) self.activateWindow() self.setVisible(True) def exit(self): # Make sure tray is not visible (Fixes #513) self.tray.setVisible(False) self.setForceClose() self.close() def quicklist(self, channels): if Dbusmenu is not None: if channels is not None: ql = Dbusmenu.Menuitem.new() self.launcher.set_property("quicklist", ql) for c in channels: if type(c) is dict and hasattr(c, '__getitem__') and c['is_member']: item = Dbusmenu.Menuitem.new () item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, "#"+c['name']) item.property_set ("id", c['name']) item.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, True) item.connect(Dbusmenu.MENUITEM_SIGNAL_ITEM_ACTIVATED, self.current().openChannel) ql.child_append(item) self.launcher.set_property("quicklist", ql) def notify(self, title, message, icon): if self.debug: print("Notification: title [{}] message [{}] icon [{}]".format(title, message, icon)) self.notifier.notify(title, message, icon) self.alert() def alert(self): if not self.isActiveWindow(): self.launcher.set_property("urgent", True) self.tray.alert() if self.urgent_hint is True: QApplication.alert(self) def count(self): total = 0 unreads = 0 for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) highlights = widget.highlights unreads+= widget.unreads total+=highlights if total > self.messages: self.alert() if 0 == total: self.launcher.set_property("count_visible", False) self.tray.setCounter(0) if unreads > 0: self.setWindowTitle("*{}".format(self.title)) else: self.setWindowTitle(self.title) else: self.tray.setCounter(total) self.launcher.set_property("count", total) self.launcher.set_property("count_visible", True) self.setWindowTitle("[{}]{}".format(str(total), self.title)) self.messages = total def clearMemory(self): QWebSettings.globalSettings().clearMemoryCaches() QWebSettings.globalSettings().clearIconDatabase()
class LiveCSS(QWidget): goto_declaration = pyqtSignal(object) def __init__(self, preview, parent=None): QWidget.__init__(self, parent) self.preview = preview self.preview_is_refreshing = False self.refresh_needed = False preview.refresh_starting.connect(self.preview_refresh_starting) preview.refreshed.connect(self.preview_refreshed) self.apply_theme() self.setAutoFillBackground(True) self.update_timer = QTimer(self) self.update_timer.timeout.connect(self.update_data) self.update_timer.setSingleShot(True) self.update_timer.setInterval(500) self.now_showing = (None, None, None) self.stack = s = QStackedLayout(self) self.setLayout(s) self.clear_label = la = QLabel('<h3>' + _( 'No style information found') + '</h3><p>' + _( 'Move the cursor inside a HTML tag to see what styles' ' apply to that tag.')) la.setWordWrap(True) la.setAlignment(Qt.AlignTop | Qt.AlignLeft) s.addWidget(la) self.box = box = Box(self) box.hyperlink_activated.connect(self.goto_declaration, type=Qt.QueuedConnection) self.scroll = sc = QScrollArea(self) sc.setWidget(box) sc.setWidgetResizable(True) s.addWidget(sc) def preview_refresh_starting(self): self.preview_is_refreshing = True def preview_refreshed(self): self.preview_is_refreshing = False # We must let the event loop run otherwise the webview will return # stale data in read_data() self.refresh_needed = True self.start_update_timer() def apply_theme(self): f = self.font() f.setFamily(tprefs['editor_font_family'] or default_font_family()) f.setPointSize(tprefs['editor_font_size']) self.setFont(f) theme = get_theme(tprefs['editor_theme']) pal = self.palette() pal.setColor(pal.Window, theme_color(theme, 'Normal', 'bg')) pal.setColor(pal.WindowText, theme_color(theme, 'Normal', 'fg')) pal.setColor(pal.AlternateBase, theme_color(theme, 'HighlightRegion', 'bg')) pal.setColor(pal.Link, theme_color(theme, 'Link', 'fg')) pal.setColor(pal.LinkVisited, theme_color(theme, 'Keyword', 'fg')) self.setPalette(pal) if hasattr(self, 'box'): self.box.relayout() self.update() def clear(self): self.stack.setCurrentIndex(0) def show_data(self, editor_name, sourceline, tags): if self.preview_is_refreshing: return if sourceline is None: self.clear() else: data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: if editor_name == self.current_name and (editor_name, sourceline, tags) == self.now_showing: # Try again in a little while in case there was a transient # error in the web view self.start_update_timer() return if self.now_showing == (None, None, None) or self.now_showing[0] != self.current_name: self.clear() return # Try to refresh the data for the currently shown tag instead # of clearing editor_name, sourceline, tags = self.now_showing data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: self.clear() return self.now_showing = (editor_name, sourceline, tags) data['html_name'] = editor_name self.box.show_data(data) self.refresh_needed = False self.stack.setCurrentIndex(1) def read_data(self, sourceline, tags): mf = self.preview.view.page().mainFrame() tags = [x.lower() for x in tags] result = unicode_type(mf.evaluateJavaScript( 'window.calibre_preview_integration.live_css(%s, %s)' % ( json.dumps(sourceline), json.dumps(tags))) or '') try: result = json.loads(result) except ValueError: result = None if result is not None: maximum_specificities = {} for node in result['nodes']: is_ancestor = node['is_ancestor'] for rule in node['css']: self.process_rule(rule, is_ancestor, maximum_specificities) for node in result['nodes']: for rule in node['css']: for prop in rule['properties']: if prop.specificity < maximum_specificities[prop.name]: prop.is_overriden = True return result def process_rule(self, rule, is_ancestor, maximum_specificities): selector = rule['selector'] sheet_index = rule['sheet_index'] rule_address = rule['rule_address'] or () if selector is not None: try: specificity = [0] + list(parse(selector)[0].specificity()) except (AttributeError, TypeError, SelectorError): specificity = [0, 0, 0, 0] else: # style attribute specificity = [1, 0, 0, 0] specificity.extend((sheet_index, tuple(rule_address))) ancestor_specificity = 0 if is_ancestor else 1 properties = [] for prop in rule['properties']: important = 1 if prop[-1] == 'important' else 0 p = Property(prop, [ancestor_specificity] + [important] + specificity) properties.append(p) if p.specificity > maximum_specificities.get(p.name, (0,0,0,0,0,0)): maximum_specificities[p.name] = p.specificity rule['properties'] = properties href = rule['href'] if hasattr(href, 'startswith') and href.startswith('%s://%s' % (FAKE_PROTOCOL, FAKE_HOST)): qurl = QUrl(href) name = qurl.path()[1:] if name: rule['href'] = name @property def current_name(self): return self.preview.current_name @property def is_visible(self): return self.isVisible() def showEvent(self, ev): self.update_timer.start() actions['auto-reload-preview'].setEnabled(True) return QWidget.showEvent(self, ev) def sync_to_editor(self): self.update_data() def update_data(self): if not self.is_visible or self.preview_is_refreshing: return editor_name = self.current_name ed = editors.get(editor_name, None) if self.update_timer.isActive() or (ed is None and editor_name is not None): return QTimer.singleShot(100, self.update_data) if ed is not None: sourceline, tags = ed.current_tag(for_position_sync=False) if self.refresh_needed or self.now_showing != (editor_name, sourceline, tags): self.show_data(editor_name, sourceline, tags) def start_update_timer(self): if self.is_visible: self.update_timer.start() def stop_update_timer(self): self.update_timer.stop() def navigate_to_declaration(self, data, editor): if data['type'] == 'inline': sourceline, tags = data['sourceline_address'] editor.goto_sourceline(sourceline, tags, attribute='style') elif data['type'] == 'sheet': editor.goto_css_rule(data['rule_address']) elif data['type'] == 'elem': editor.goto_css_rule(data['rule_address'], sourceline_address=data['sourceline_address'])
class HostQWidget(QWidget): """ Class who create QWidget to display host data """ def __init__(self, parent=None): super(HostQWidget, self).__init__(parent) # Fields self.actions_widget = ActionsQWidget() self.host_item = None self.service_items = None self.labels = { 'host_icon': QLabel(), 'host_name': QLabel(), 'state_icon': QLabel(), 'ls_last_check': QLabel(), 'ls_output': QLabel(), 'realm': QLabel(), 'address': QLabel(), 'business_impact': QLabel(), 'notes': QLabel() } self.activecheck_btn = ToggleQWidgetButton() self.passivecheck_btn = ToggleQWidgetButton() self.history_btn = QPushButton() self.history_widget = HistoryQWidget() self.host_history = None self.customs_widget = CustomsQWidget() self.customs_btn = QPushButton() self.spy_btn = QPushButton() self.refresh_timer = QTimer() self.refresh_counter = 0 def initialize(self): """ Initialize QWidget """ layout = QGridLayout() self.setLayout(layout) # Add Qwidgets layout.addWidget(self.get_host_icon_widget(), 0, 0, 2, 1) layout.addWidget(self.get_last_check_widget(), 0, 1, 1, 1) layout.addWidget(self.get_variables_widget(), 0, 2, 1, 1) layout.addWidget(self.get_notes_output_widget(), 1, 1, 1, 2) layout.addWidget(self.get_actions_widget(), 0, 3, 2, 1) update_host = int(settings.get_config('Alignak-app', 'update_host')) * 1000 self.refresh_timer.setInterval(update_host) self.refresh_timer.start() self.refresh_timer.timeout.connect(self.update_host) def set_data(self, host_item): """ Set data of host and service :param host_item: the Host item :type host_item: alignak_app.items.host.Host """ # Query services of host self.host_item = host_item # Get problems host_and_services = data_manager.get_host_with_services(host_item.item_id) self.host_item = host_and_services['host'] self.service_items = host_and_services['services'] def get_host_icon_widget(self): """ Return QWidget with overall icon state and host name :return: widget with host name and icon :rtype: QWidget """ widget = QWidget() layout = QVBoxLayout() widget.setLayout(layout) # Host Icon layout.addWidget(self.labels['host_icon']) layout.setAlignment(self.labels['host_icon'], Qt.AlignCenter) # Host Name self.labels['host_name'].setObjectName('itemname') self.labels['host_name'].setWordWrap(True) layout.addWidget(self.labels['host_name']) layout.setAlignment(self.labels['host_name'], Qt.AlignCenter) # Customs button customs_lbl = QLabel(_('Configuration:')) customs_lbl.setObjectName('subtitle') layout.addWidget(customs_lbl) layout.setAlignment(customs_lbl, Qt.AlignBottom) self.customs_btn.setIcon(QIcon(settings.get_image('settings'))) self.customs_btn.setFixedSize(80, 20) self.customs_btn.clicked.connect(self.show_customs) layout.addWidget(self.customs_btn) layout.setAlignment(self.customs_btn, Qt.AlignCenter) # Initialize Customs QWidget self.customs_widget.initialize() return widget def get_actions_widget(self): """ Return QWidget with actions buttons :return: widget with buttons :rtype: QWidget """ widget = QWidget() layout = QVBoxLayout() widget.setLayout(layout) # Actions action_title = QLabel(_('Host actions')) action_title.setObjectName('itemtitle') action_title.setFixedHeight(25) layout.addWidget(action_title) ack_down_lbl = QLabel(_('Acknowledge / Downtime:')) ack_down_lbl.setObjectName('subtitle') layout.addWidget(ack_down_lbl) self.actions_widget.initialize(self.host_item) layout.addWidget(self.actions_widget) # Active Checks activecheck_lbl = QLabel(_('Active checks:')) activecheck_lbl.setObjectName('subtitle') layout.addWidget(activecheck_lbl) self.activecheck_btn.initialize() self.activecheck_btn.toggle_btn.clicked.connect(lambda: self.patch_host_checks( 'active_checks_enabled', self.activecheck_btn.is_checked() )) layout.addWidget(self.activecheck_btn) # Passive Checks passivecheck_lbl = QLabel(_('Passive checks:')) passivecheck_lbl.setObjectName('subtitle') layout.addWidget(passivecheck_lbl) self.passivecheck_btn.initialize() self.passivecheck_btn.toggle_btn.clicked.connect(lambda: self.patch_host_checks( 'passive_checks_enabled', self.passivecheck_btn.is_checked() )) layout.addWidget(self.passivecheck_btn) # History hist_lbl = QLabel(_('Timeline:')) hist_lbl.setObjectName('subtitle') layout.addWidget(hist_lbl) self.history_btn.setIcon(QIcon(settings.get_image('time'))) self.history_btn.setFixedSize(80, 20) self.history_btn.clicked.connect(self.show_history) self.history_btn.setToolTip(_('See history of host')) layout.addWidget(self.history_btn) layout.setAlignment(self.history_btn, Qt.AlignCenter) self.history_widget.initialize() # Spy Button spy_lbl = QLabel(_('Spy Host:')) spy_lbl.setObjectName('subtitle') layout.addWidget(spy_lbl) self.spy_btn.setIcon(QIcon(settings.get_image('spy'))) self.spy_btn.setFixedSize(80, 20) self.spy_btn.setToolTip(_('Spy current host')) layout.addWidget(self.spy_btn) layout.setAlignment(self.spy_btn, Qt.AlignCenter) layout.setAlignment(Qt.AlignCenter | Qt.AlignTop) return widget def get_last_check_widget(self): """ Return QWidget with last check data :return: widget with last check data :rtype: QWidget """ widget = QWidget() layout = QGridLayout() widget.setLayout(layout) # Title check_title = QLabel(_('My last check')) check_title.setObjectName('itemtitle') check_title.setFixedHeight(25) layout.addWidget(check_title, 0, 0, 1, 2) # State state_title = QLabel(_("State:")) state_title.setObjectName('subtitle') layout.addWidget(state_title, 1, 0, 1, 1) layout.addWidget(self.labels['state_icon'], 1, 1, 1, 1) # When last check when_title = QLabel(_("When:")) when_title.setObjectName('subtitle') layout.addWidget(when_title, 2, 0, 1, 1) layout.addWidget(self.labels['ls_last_check'], 2, 1, 1, 1) return widget def get_variables_widget(self): """ Return QWidget with host variables :return: widget with host variables :rtype: QWidget """ widget = QWidget() layout = QGridLayout() widget.setLayout(layout) # Title check_title = QLabel(_('My variables')) check_title.setObjectName('itemtitle') check_title.setFixedHeight(25) layout.addWidget(check_title, 0, 0, 1, 2) # Realm realm_title = QLabel(_("Realm:")) realm_title.setObjectName('subtitle') layout.addWidget(realm_title, 1, 0, 1, 1) layout.addWidget(self.labels['realm'], 1, 1, 1, 1) # Address address_title = QLabel(_("Host address:")) address_title.setObjectName('subtitle') layout.addWidget(address_title, 2, 0, 1, 1) layout.addWidget(self.labels['address'], 2, 1, 1, 1) # Business impact business_title = QLabel(_("Business impact:")) business_title.setObjectName('subtitle') layout.addWidget(business_title, 3, 0, 1, 1) layout.addWidget(self.labels['business_impact'], 3, 1, 1, 1) return widget def get_notes_output_widget(self): """ Return QWidget with output and notes data :return: widget with host output and notes :rtype: QWidget """ widget = QWidget() layout = QGridLayout() widget.setLayout(layout) # Output output_title = QLabel(_("Output")) output_title.setObjectName('title') layout.addWidget(output_title, 0, 0, 1, 1) self.labels['ls_output'].setWordWrap(True) self.labels['ls_output'].setTextInteractionFlags(Qt.TextSelectableByMouse) output_scrollarea = QScrollArea() output_scrollarea.setWidget(self.labels['ls_output']) output_scrollarea.setWidgetResizable(True) output_scrollarea.setObjectName('output') layout.addWidget(output_scrollarea, 1, 0, 1, 2) # Notes notes_title = QLabel(_("Notes:")) notes_title.setObjectName('title') layout.addWidget(notes_title, 0, 2, 1, 1) notes_btn = QPushButton() notes_btn.setIcon(QIcon(settings.get_image('edit'))) notes_btn.setToolTip(_("Edit host notes.")) notes_btn.setFixedSize(32, 32) notes_btn.clicked.connect(self.patch_data) layout.addWidget(notes_btn, 0, 3, 1, 1) self.labels['notes'].setWordWrap(True) self.labels['notes'].setTextInteractionFlags(Qt.TextSelectableByMouse) notes_scrollarea = QScrollArea() notes_scrollarea.setWidget(self.labels['notes']) notes_scrollarea.setWidgetResizable(True) notes_scrollarea.setObjectName('notes') layout.addWidget(notes_scrollarea, 1, 2, 1, 2) return widget def show_history(self): """ Update and show HistoryQWidget """ self.history_widget.update_history_data(self.host_item.name, self.host_history) self.history_widget.show() def show_customs(self): """ Update and show CustomsQWidget """ self.customs_widget.update_customs(self.host_item) self.customs_widget.show() def patch_host_checks(self, check_type, state): # pragma: no cover """ Patch the host check: 'active_checks_enabled' | 'passive_checks_enabled' :param check_type: type of check: 'active_checks_enabled' | 'passive_checks_enabled' :type check_type: str :param state: state of Toggle button :type state: bool """ data = {check_type: state} headers = {'If-Match': self.host_item.data['_etag']} endpoint = '/'.join([self.host_item.item_type, self.host_item.item_id]) patched = app_backend.patch(endpoint, data, headers) if patched: self.host_item.data[check_type] = state data_manager.update_item_data( self.host_item.item_type, self.host_item.item_id, self.host_item.data ) enabled = _('enabled') if state else _('disabled') event_type = 'OK' if state else 'WARN' message = _( _('%s %s for %s' % (Item.get_check_text(check_type), enabled, self.host_item.get_display_name())) ) send_event(event_type, message, timer=True) else: send_event( 'ERROR', _("Backend PATCH failed, please check your logs !") ) def patch_data(self): # pragma: no cover """ Display QDialog for patch """ notes_dialog = EditQDialog() notes_dialog.initialize( _('Edit Host Notes'), self.host_item.data['notes'] ) if notes_dialog.exec_() == EditQDialog.Accepted: data = {'notes': str(notes_dialog.text_edit.toPlainText())} headers = {'If-Match': self.host_item.data['_etag']} endpoint = '/'.join(['host', self.host_item.item_id]) patched = app_backend.patch(endpoint, data, headers) if patched: data_manager.update_item_data( self.host_item.item_type, self.host_item.item_id, {'notes': notes_dialog.text_edit.toPlainText()} ) self.labels['notes'].setText(notes_dialog.text_edit.toPlainText()) message = _( _("Host notes have been edited.") ) send_event('INFO', message) else: send_event( 'ERROR', _("Backend PATCH failed, please check your logs !") ) def update_host(self, host_item=None): """ Update HostQWidget data and QLabels :param host_item: the Host item :type host_item: alignak_app.items.host.Host """ if self.host_item and not host_item: self.set_data(self.host_item) if host_item: self.set_data(host_item) if self.host_item or host_item: # Update host services self.refresh_counter += 1 if self.refresh_counter > 10: thread_manager.add_high_priority_thread('service', self.host_item.item_id) self.refresh_counter = 0 # Update host icon_name = get_overall_state_icon( self.service_items, self.host_item.data['_overall_state_id'] ) icon_pixmap = QPixmap(settings.get_image(icon_name)) self.labels['host_icon'].setPixmap(QPixmap(icon_pixmap)) self.labels['host_icon'].setToolTip( self.host_item.get_overall_tooltip(self.service_items) ) self.labels['host_name'].setText('%s' % self.host_item.get_display_name()) monitored = self.host_item.data[ 'passive_checks_enabled'] + self.host_item.data['active_checks_enabled'] icon_name = get_icon_name( 'host', self.host_item.data['ls_state'], self.host_item.data['ls_acknowledged'], self.host_item.data['ls_downtimed'], monitored ) pixmap_icon = QPixmap(settings.get_image(icon_name)) final_icon = pixmap_icon.scaled(32, 32, Qt.KeepAspectRatio) self.labels['state_icon'].setPixmap(final_icon) self.labels['state_icon'].setToolTip(self.host_item.get_tooltip()) since_last_check = get_diff_since_last_timestamp( self.host_item.data['ls_last_check'] ) last_check_tooltip = get_date_fromtimestamp(self.host_item.data['ls_last_check']) self.labels['ls_last_check'].setText(since_last_check) self.labels['ls_last_check'].setToolTip(last_check_tooltip) self.labels['ls_output'].setText(self.host_item.data['ls_output']) self.labels['realm'].setText( data_manager.get_realm_name(self.host_item.data['_realm']) ) self.labels['address'].setText(self.host_item.data['address']) self.labels['business_impact'].setText(str(self.host_item.data['business_impact'])) self.labels['notes'].setText(self.host_item.data['notes']) self.actions_widget.item = self.host_item self.actions_widget.update_widget() self.activecheck_btn.update_btn_state(self.host_item.data['active_checks_enabled']) self.passivecheck_btn.update_btn_state(self.host_item.data['passive_checks_enabled']) self.customs_btn.setEnabled(bool(self.host_item.data['customs'])) # Update host history self.host_history = data_manager.get_item('history', self.host_item.item_id) if self.host_history: self.history_btn.setEnabled(True) self.history_btn.setToolTip(_('History is available')) else: self.history_btn.setToolTip(_('History is not available, please wait...')) self.history_btn.setEnabled(False) if app_backend.connected: thread_manager.add_high_priority_thread( 'history', {'hostname': self.host_item.name, 'host_id': self.host_item.item_id} ) else: thread_manager.stop_threads()
class Splitter(QSplitter): state_changed = pyqtSignal(object) def __init__(self, name, label, icon, initial_show=True, initial_side_size=120, connect_button=True, orientation=Qt.Horizontal, side_index=0, parent=None, shortcut=None, hide_handle_on_single_panel=True): QSplitter.__init__(self, parent) if hide_handle_on_single_panel: self.state_changed.connect(self.update_handle_width) self.original_handle_width = self.handleWidth() self.resize_timer = QTimer(self) self.resize_timer.setSingleShot(True) self.desired_side_size = initial_side_size self.desired_show = initial_show self.resize_timer.setInterval(5) self.resize_timer.timeout.connect(self.do_resize) self.setOrientation(orientation) self.side_index = side_index self._name = name self.label = label self.initial_side_size = initial_side_size self.initial_show = initial_show self.splitterMoved.connect(self.splitter_moved, type=Qt.QueuedConnection) self.button = LayoutButton(icon, label, self, shortcut=shortcut) if connect_button: self.button.clicked.connect(self.double_clicked) if shortcut is not None: self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label, self) self.action_toggle.changed.connect(self.update_shortcut) self.action_toggle.triggered.connect(self.toggle_triggered) if parent is not None: parent.addAction(self.action_toggle) if hasattr(parent, 'keyboard'): parent.keyboard.register_shortcut('splitter %s %s'%(name, label), unicode_type(self.action_toggle.text()), default_keys=(shortcut,), action=self.action_toggle) else: self.action_toggle.setShortcut(shortcut) else: self.action_toggle.setShortcut(shortcut) def update_shortcut(self): self.button.update_shortcut(self.action_toggle) def toggle_triggered(self, *args): self.toggle_side_pane() def createHandle(self): return SplitterHandle(self.orientation(), self) def initialize(self): for i in range(self.count()): h = self.handle(i) if h is not None: h.splitter_moved() self.state_changed.emit(not self.is_side_index_hidden) def splitter_moved(self, *args): self.desired_side_size = self.side_index_size self.state_changed.emit(not self.is_side_index_hidden) def update_handle_width(self, not_one_panel): self.setHandleWidth(self.original_handle_width if not_one_panel else 0) @property def is_side_index_hidden(self): sizes = list(self.sizes()) try: return sizes[self.side_index] == 0 except IndexError: return True @property def save_name(self): ori = 'horizontal' if self.orientation() == Qt.Horizontal \ else 'vertical' return self._name + '_' + ori def print_sizes(self): if self.count() > 1: print(self.save_name, 'side:', self.side_index_size, 'other:', end=' ') print(list(self.sizes())[self.other_index]) @dynamic_property def side_index_size(self): def fget(self): if self.count() < 2: return 0 return self.sizes()[self.side_index] def fset(self, val): if self.count() < 2: return if val == 0 and not self.is_side_index_hidden: self.save_state() sizes = list(self.sizes()) for i in range(len(sizes)): sizes[i] = val if i == self.side_index else 10 self.setSizes(sizes) total = sum(self.sizes()) sizes = list(self.sizes()) for i in range(len(sizes)): sizes[i] = val if i == self.side_index else total-val self.setSizes(sizes) self.initialize() return property(fget=fget, fset=fset) def do_resize(self, *args): orig = self.desired_side_size QSplitter.resizeEvent(self, self._resize_ev) if orig > 20 and self.desired_show: c = 0 while abs(self.side_index_size - orig) > 10 and c < 5: self.apply_state(self.get_state(), save_desired=False) c += 1 def resizeEvent(self, ev): if self.resize_timer.isActive(): self.resize_timer.stop() self._resize_ev = ev self.resize_timer.start() def get_state(self): if self.count() < 2: return (False, 200) return (self.desired_show, self.desired_side_size) def apply_state(self, state, save_desired=True): if state[0]: self.side_index_size = state[1] if save_desired: self.desired_side_size = self.side_index_size else: self.side_index_size = 0 self.desired_show = state[0] def default_state(self): return (self.initial_show, self.initial_side_size) # Public API {{{ def update_desired_state(self): self.desired_show = not self.is_side_index_hidden def save_state(self): if self.count() > 1: gprefs[self.save_name+'_state'] = self.get_state() @property def other_index(self): return (self.side_index+1)%2 def restore_state(self): if self.count() > 1: state = gprefs.get(self.save_name+'_state', self.default_state()) self.apply_state(state, save_desired=False) self.desired_side_size = state[1] def toggle_side_pane(self, hide=None): if hide is None: action = 'show' if self.is_side_index_hidden else 'hide' else: action = 'hide' if hide else 'show' getattr(self, action+'_side_pane')() def show_side_pane(self): if self.count() < 2 or not self.is_side_index_hidden: return if self.desired_side_size == 0: self.desired_side_size = self.initial_side_size self.apply_state((True, self.desired_side_size)) def hide_side_pane(self): if self.count() < 2 or self.is_side_index_hidden: return self.apply_state((False, self.desired_side_size)) def double_clicked(self, *args): self.toggle_side_pane()
class MaterialManager(QObject): materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated. def __init__(self, container_registry, parent = None): super().__init__(parent) self._application = Application.getInstance() self._container_registry = container_registry # type: ContainerRegistry self._fallback_materials_map = dict() # material_type -> generic material metadata self._material_group_map = dict() # root_material_id -> MaterialGroup self._diameter_machine_variant_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode) # We're using these two maps to convert between the specific diameter material id and the generic material id # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant # i.e. generic_pla -> generic_pla_175 self._material_diameter_map = defaultdict(dict) # root_material_id -> approximate diameter str -> root_material_id for that diameter self._diameter_material_map = dict() # material id including diameter (generic_pla_175) -> material root id (generic_pla) # This is used in Legacy UM3 send material function and the material management page. self._guid_material_groups_map = defaultdict(list) # GUID -> a list of material_groups # The machine definition ID for the non-machine-specific materials. # This is used as the last fallback option if the given machine-specific material(s) cannot be found. self._default_machine_definition_id = "fdmprinter" self._default_approximate_diameter_for_quality_search = "3" # When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't # want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't # react too many time. self._update_timer = QTimer(self) self._update_timer.setInterval(300) self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self._updateMaps) self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged) self._container_registry.containerAdded.connect(self._onContainerMetadataChanged) self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged) def initialize(self): # Find all materials and put them in a matrix for quick search. material_metadatas = {metadata["id"]: metadata for metadata in self._container_registry.findContainersMetadata(type = "material") if metadata.get("GUID")} self._material_group_map = dict() # Map #1 # root_material_id -> MaterialGroup for material_id, material_metadata in material_metadatas.items(): # We don't store empty material in the lookup tables if material_id == "empty_material": continue root_material_id = material_metadata.get("base_file") if root_material_id not in self._material_group_map: self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id])) self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id) group = self._material_group_map[root_material_id] # Store this material in the group of the appropriate root material. if material_id != root_material_id: new_node = MaterialNode(material_metadata) group.derived_material_node_list.append(new_node) # Order this map alphabetically so it's easier to navigate in a debugger self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0])) # Map #1.5 # GUID -> material group list self._guid_material_groups_map = defaultdict(list) for root_material_id, material_group in self._material_group_map.items(): guid = material_group.root_material_node.metadata["GUID"] self._guid_material_groups_map[guid].append(material_group) # Map #2 # Lookup table for material type -> fallback material metadata, only for read-only materials grouped_by_type_dict = dict() material_types_without_fallback = set() for root_material_id, material_node in self._material_group_map.items(): material_type = material_node.root_material_node.metadata["material"] if material_type not in grouped_by_type_dict: grouped_by_type_dict[material_type] = {"generic": None, "others": []} material_types_without_fallback.add(material_type) brand = material_node.root_material_node.metadata["brand"] if brand.lower() == "generic": to_add = True if material_type in grouped_by_type_dict: diameter = material_node.root_material_node.metadata.get("approximate_diameter") if diameter != self._default_approximate_diameter_for_quality_search: to_add = False # don't add if it's not the default diameter if to_add: # Checking this first allow us to differentiate between not read only materials: # - if it's in the list, it means that is a new material without fallback # - if it is not, then it is a custom material with a fallback material (parent) if material_type in material_types_without_fallback: grouped_by_type_dict[material_type] = material_node.root_material_node.metadata material_types_without_fallback.remove(material_type) # Remove the materials that have no fallback materials for material_type in material_types_without_fallback: del grouped_by_type_dict[material_type] self._fallback_materials_map = grouped_by_type_dict # Map #3 # There can be multiple material profiles for the same material with different diameters, such as "generic_pla" # and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can # be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID # for quality search. self._material_diameter_map = defaultdict(dict) self._diameter_material_map = dict() # Group the material IDs by the same name, material, brand, and color but with different diameters. material_group_dict = dict() keys_to_fetch = ("name", "material", "brand", "color") for root_material_id, machine_node in self._material_group_map.items(): root_material_metadata = machine_node.root_material_node.metadata key_data = [] for key in keys_to_fetch: key_data.append(root_material_metadata.get(key)) key_data = tuple(key_data) # If the key_data doesn't exist, it doesn't matter if the material is read only... if key_data not in material_group_dict: material_group_dict[key_data] = dict() else: # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it if not machine_node.is_read_only: continue approximate_diameter = root_material_metadata.get("approximate_diameter") material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"] # Map [root_material_id][diameter] -> root_material_id for this diameter for data_dict in material_group_dict.values(): for root_material_id1 in data_dict.values(): if root_material_id1 in self._material_diameter_map: continue diameter_map = data_dict for root_material_id2 in data_dict.values(): self._material_diameter_map[root_material_id2] = diameter_map default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search) if default_root_material_id is None: default_root_material_id = list(data_dict.values())[0] # no default diameter present, just take "the" only one for root_material_id in data_dict.values(): self._diameter_material_map[root_material_id] = default_root_material_id # Map #4 # "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer # Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer self._diameter_machine_variant_material_map = dict() for material_metadata in material_metadatas.values(): # We don't store empty material in the lookup tables if material_metadata["id"] == "empty_material": continue root_material_id = material_metadata["base_file"] definition = material_metadata["definition"] approximate_diameter = material_metadata["approximate_diameter"] if approximate_diameter not in self._diameter_machine_variant_material_map: self._diameter_machine_variant_material_map[approximate_diameter] = {} machine_variant_material_map = self._diameter_machine_variant_material_map[approximate_diameter] if definition not in machine_variant_material_map: machine_variant_material_map[definition] = MaterialNode() machine_node = machine_variant_material_map[definition] variant_name = material_metadata.get("variant_name") if not variant_name: # if there is no variant, this material is for the machine, so put its metadata in the machine node. machine_node.material_map[root_material_id] = MaterialNode(material_metadata) else: # this material is variant-specific, so we save it in a variant-specific node under the # machine-specific node # Check first if the variant exist in the manager existing_variant = self._application.getVariantManager().getVariantNode(definition, variant_name) if existing_variant is not None: if variant_name not in machine_node.children_map: machine_node.children_map[variant_name] = MaterialNode() variant_node = machine_node.children_map[variant_name] if root_material_id in variant_node.material_map: # We shouldn't have duplicated variant-specific materials for the same machine. ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id) continue variant_node.material_map[root_material_id] = MaterialNode(material_metadata) else: # Add this container id to the wrong containers list in the registry Logger.log("w", "Not adding {id} to the material manager because the variant does not exist.".format(id = material_metadata["id"])) self._container_registry.addWrongContainerId(material_metadata["id"]) self.materialsUpdated.emit() def _updateMaps(self): Logger.log("i", "Updating material lookup data ...") self.initialize() def _onContainerMetadataChanged(self, container): self._onContainerChanged(container) def _onContainerChanged(self, container): container_type = container.getMetaDataEntry("type") if container_type != "material": return # update the maps self._update_timer.start() def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]: return self._material_group_map.get(root_material_id) def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str: return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id) def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: return self._diameter_material_map.get(root_material_id) def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]: return self._guid_material_groups_map.get(guid) # # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # def getAvailableMaterials(self, machine_definition: "DefinitionContainer", extruder_variant_name: Optional[str], diameter: float) -> dict: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_variant_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter) return dict() machine_definition_id = machine_definition.getId() # If there are variant materials, get the variant material machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter] machine_node = machine_variant_material_map.get(machine_definition_id) default_machine_node = machine_variant_material_map.get(self._default_machine_definition_id) variant_node = None if extruder_variant_name is not None and machine_node is not None: variant_node = machine_node.getChildNode(extruder_variant_name) nodes_to_check = [variant_node, machine_node, default_machine_node] # Fallback mechanism of finding materials: # 1. variant-specific material # 2. machine-specific material # 3. generic material (for fdmprinter) machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", []) material_id_metadata_dict = dict() for node in nodes_to_check: if node is not None: # Only exclude the materials that are explicitly specified in the "exclude_materials" field. # Do not exclude other materials that are of the same type. for material_id, node in node.material_map.items(): if material_id in machine_exclude_materials: Logger.log("d", "Exclude material [%s] for machine [%s]", material_id, machine_definition.getId()) continue if material_id not in material_id_metadata_dict: material_id_metadata_dict[material_id] = node return material_id_metadata_dict # # A convenience function to get available materials for the given machine with the extruder position. # def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack", extruder_stack: "ExtruderStack") -> Optional[dict]: variant_name = None if extruder_stack.variant.getId() != "empty_variant": variant_name = extruder_stack.variant.getName() diameter = extruder_stack.approximateMaterialDiameter # Fetch the available materials (ContainerNode) for the current active machine and extruder setup. return self.getAvailableMaterials(machine.definition, variant_name, diameter) # # Gets MaterialNode for the given extruder and machine with the given material name. # Returns None if: # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNode(self, machine_definition_id: str, extruder_variant_name: Optional[str], diameter: float, root_material_id: str) -> Optional["InstanceContainer"]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_variant_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]", diameter, rounded_diameter, root_material_id) return None # If there are variant materials, get the variant material machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter] machine_node = machine_variant_material_map.get(machine_definition_id) variant_node = None # Fallback for "fdmprinter" if the machine-specific materials cannot be found if machine_node is None: machine_node = machine_variant_material_map.get(self._default_machine_definition_id) if machine_node is not None and extruder_variant_name is not None: variant_node = machine_node.getChildNode(extruder_variant_name) # Fallback mechanism of finding materials: # 1. variant-specific material # 2. machine-specific material # 3. generic material (for fdmprinter) nodes_to_check = [variant_node, machine_node, machine_variant_material_map.get(self._default_machine_definition_id)] material_node = None for node in nodes_to_check: if node is not None: material_node = node.material_map.get(root_material_id) if material_node: break return material_node # # Gets MaterialNode for the given extruder and machine with the given material type. # Returns None if: # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNodeByType(self, global_stack: "GlobalStack", extruder_variant_name: str, material_guid: str) -> Optional["MaterialNode"]: node = None machine_definition = global_stack.definition if parseBool(machine_definition.getMetaDataEntry("has_materials", False)): material_diameter = machine_definition.getProperty("material_diameter", "value") if isinstance(material_diameter, SettingFunction): material_diameter = material_diameter(global_stack) # Look at the guid to material dictionary root_material_id = None for material_group in self._guid_material_groups_map[material_guid]: if material_group.is_read_only: root_material_id = material_group.root_material_node.metadata["id"] break if not root_material_id: Logger.log("i", "Cannot find materials with guid [%s] ", material_guid) return None node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name, material_diameter, root_material_id) return node # # Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla". # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use # the generic material IDs to search for qualities. # # An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its # extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine. # A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will # be "generic_pla". This function is intended to get a generic fallback material for the given material type. # # This function returns the generic root material ID for the given material type, where material types are "PLA", # "ABS", etc. # def getFallbackMaterialIdByMaterialType(self, material_type: str) -> Optional[str]: # For safety if material_type not in self._fallback_materials_map: Logger.log("w", "The material type [%s] does not have a fallback material" % material_type) return None fallback_material = self._fallback_materials_map[material_type] if fallback_material: return self.getRootMaterialIDWithoutDiameter(fallback_material["id"]) else: return None def getDefaultMaterial(self, global_stack: "GlobalStack", extruder_variant_name: Optional[str]) -> Optional["MaterialNode"]: node = None machine_definition = global_stack.definition if parseBool(global_stack.getMetaDataEntry("has_materials", False)): material_diameter = machine_definition.getProperty("material_diameter", "value") if isinstance(material_diameter, SettingFunction): material_diameter = material_diameter(global_stack) approximate_material_diameter = str(round(material_diameter)) root_material_id = machine_definition.getMetaDataEntry("preferred_material") root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter) node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name, material_diameter, root_material_id) return node def removeMaterialByRootId(self, root_material_id: str): material_group = self.getMaterialGroup(root_material_id) if not material_group: Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id) return nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list for node in nodes_to_remove: self._container_registry.removeContainer(node.metadata["id"]) # # Methods for GUI # # # Sets the new name for the given material. # @pyqtSlot("QVariant", str) def setMaterialName(self, material_node: "MaterialNode", name: str): root_material_id = material_node.metadata["base_file"] if self._container_registry.isReadOnly(root_material_id): Logger.log("w", "Cannot set name of read-only container %s.", root_material_id) return material_group = self.getMaterialGroup(root_material_id) if material_group: material_group.root_material_node.getContainer().setName(name) # # Removes the given material. # @pyqtSlot("QVariant") def removeMaterial(self, material_node: "MaterialNode"): root_material_id = material_node.metadata["base_file"] self.removeMaterialByRootId(root_material_id) # # Creates a duplicate of a material, which has the same GUID and base_file metadata. # Returns the root material ID of the duplicated material if successful. # @pyqtSlot("QVariant", result = str) def duplicateMaterial(self, material_node, new_base_id = None, new_metadata = None) -> Optional[str]: root_material_id = material_node.metadata["base_file"] material_group = self.getMaterialGroup(root_material_id) if not material_group: Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id) return None base_container = material_group.root_material_node.getContainer() if not base_container: return None # Ensure all settings are saved. self._application.saveSettings() # Create a new ID & container to hold the data. new_containers = [] if new_base_id is None: new_base_id = self._container_registry.uniqueName(base_container.getId()) new_base_container = copy.deepcopy(base_container) new_base_container.getMetaData()["id"] = new_base_id new_base_container.getMetaData()["base_file"] = new_base_id if new_metadata is not None: for key, value in new_metadata.items(): new_base_container.getMetaData()[key] = value new_containers.append(new_base_container) # Clone all of them. for node in material_group.derived_material_node_list: container_to_copy = node.getContainer() if not container_to_copy: continue # Create unique IDs for every clone. new_id = new_base_id if container_to_copy.getMetaDataEntry("definition") != "fdmprinter": new_id += "_" + container_to_copy.getMetaDataEntry("definition") if container_to_copy.getMetaDataEntry("variant_name"): variant_name = container_to_copy.getMetaDataEntry("variant_name") new_id += "_" + variant_name.replace(" ", "_") new_container = copy.deepcopy(container_to_copy) new_container.getMetaData()["id"] = new_id new_container.getMetaData()["base_file"] = new_base_id if new_metadata is not None: for key, value in new_metadata.items(): new_container.getMetaData()[key] = value new_containers.append(new_container) for container_to_add in new_containers: container_to_add.setDirty(True) self._container_registry.addContainer(container_to_add) return new_base_id # # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID. # @pyqtSlot(result = str) def createMaterial(self) -> str: from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") # Ensure all settings are saved. self._application.saveSettings() machine_manager = self._application.getMachineManager() extruder_stack = machine_manager.activeStack approximate_diameter = str(extruder_stack.approximateMaterialDiameter) root_material_id = "generic_pla" root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter) material_group = self.getMaterialGroup(root_material_id) # Create a new ID & container to hold the data. new_id = self._container_registry.uniqueName("custom_material") new_metadata = {"name": catalog.i18nc("@label", "Custom Material"), "brand": catalog.i18nc("@label", "Custom"), "GUID": str(uuid.uuid4()), } self.duplicateMaterial(material_group.root_material_node, new_base_id = new_id, new_metadata = new_metadata) return new_id
class EventsQWidget(QWidget): """ Class who create QWidget for events """ def __init__(self): super(EventsQWidget, self).__init__() self.setObjectName('events') # Fields self.events_list = QListWidget() self.timer = QTimer() def initialize(self): """ Intialize QWidget """ self.timer.setInterval(30000) self.timer.start() self.timer.timeout.connect(self.send_datamanager_events) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self.events_list.setDragDropMode(QAbstractItemView.DragOnly) self.events_list.setSelectionMode(QAbstractItemView.ExtendedSelection) self.events_list.setDropIndicatorShown(True) self.events_list.doubleClicked.connect(self.remove_event) self.events_list.setWordWrap(True) self.events_list.setIconSize(QSize(16, 16)) self.add_event( 'OK', _('Welcome %s, you are connected to Alignak Backend') % data_manager.database['user'].name, timer=True ) layout.addWidget(self.events_list) def send_datamanager_events(self): """ Add events stored in DataManager """ events = data_manager.get_events() if events: for event in events: self.add_event( event['event_type'], event['message'], timer=False, host=event['host'] ) def add_event(self, event_type, msg, timer=False, host=None): """ Add event to events list :param event_type: the type of event: OK, DOWN, ACK, ... :type event_type: str :param msg: message of event :type msg: str :param timer: timer to hide event at end of time :type timer: bool :param host: data of a host to set ``Qt.UserRole`` :type host: None | str """ if not self.event_exist(msg): logger.debug( 'Add Event: msg: %s, timer: %s, host: %s', msg, timer, host ) event = EventItem() event.initialize(event_type, msg, timer=timer, host=host) self.events_list.insertItem(0, event) if timer: event_duration = int( settings.get_config('Alignak-app', 'notification_duration') ) * 1000 QTimer.singleShot( event_duration, lambda: self.remove_timer_event(event) ) else: logger.debug( 'Event with msg: %s already exist.', msg ) def event_exist(self, msg): """ Check if event already displayed, move it to top and update tooltip. Only for EventItem who have a ``Qt.UserRole`` :param msg: message of event :type msg: str :return: if message exist or not in events QWidgetList :rtype: bool """ for i in range(0, self.events_list.count()): if self.events_list.item(i).data(Qt.DisplayRole) == msg: item = self.events_list.takeItem(i) msg_to_send = '%s. (Send at %s)' % (msg, get_current_time()) item.setToolTip(msg_to_send) self.events_list.insertItem(0, item) return True return False def remove_timer_event(self, event): """ Remove EventItem with timer :param event: EventItem with timer :type event: EventItem """ logger.debug('Remove Timer Event: %s', event.text()) self.events_list.takeItem(self.events_list.row(event)) def remove_event(self, item=None): """ Remove item when user double click on an item :param item: item to remove, else remove the current row :type item: EventItem """ if isinstance(item, EventItem): row = self.events_list.row(item) self.events_list.takeItem(row) else: item = self.events_list.takeItem(self.events_list.currentRow()) logger.debug('Remove Event: %s', item.text())
class AlignakQWidget(QWidget): """ Class who display daemons status, backend connection and user informations """ def __init__(self, parent=None): super(AlignakQWidget, self).__init__(parent) # Fields self.backend_connected = QLabel('pending...') self.status_dialog = StatusQDialog() self.status_btn = QPushButton() self.ws_connected = QLabel() self.profile_widget = ProfileQWidget() self.profile_btn = QPushButton() self.refresh_timer = QTimer() def initialize(self): """ Initialize QWidget """ self.update_status() layout = QGridLayout() self.setLayout(layout) # Backend state connected_title = QLabel(_('Backend API')) connected_title.setObjectName('subtitle') layout.addWidget(connected_title, 0, 0, 1, 1) self.backend_connected.setFixedSize(16, 16) self.backend_connected.setScaledContents(True) self.backend_connected.setToolTip(_('Status of the backend API connection')) layout.addWidget(self.backend_connected, 0, 1, 1, 1) layout.setAlignment(self.backend_connected, Qt.AlignCenter) # WS state ws_title = QLabel(_('Web Service')) ws_title.setObjectName('subtitle') layout.addWidget(ws_title, 0, 2, 1, 1) self.ws_connected.setFixedSize(16, 16) self.ws_connected.setScaledContents(True) self.ws_connected.setToolTip(_('Status of the Web Service connection')) layout.addWidget(self.ws_connected, 0, 3, 1, 1) layout.setAlignment(self.ws_connected, Qt.AlignCenter) # Daemons state daemons_title = QLabel(_('Alignak')) daemons_title.setObjectName('subtitle') layout.addWidget(daemons_title, 1, 0, 1, 1) self.status_dialog.initialize() self.update_status_btn(self.status_dialog.update_dialog()) self.status_btn.setFixedSize(32, 32) self.status_btn.clicked.connect(self.show_status_dialog) self.status_btn.setToolTip(_('Status of daemons connection')) layout.addWidget(self.status_btn, 1, 1, 1, 1) layout.setAlignment(self.status_btn, Qt.AlignCenter) # User user_lbl = QLabel(_('User')) user_lbl.setObjectName('subtitle') layout.addWidget(user_lbl, 1, 2, 1, 1) self.profile_widget.initialize() self.profile_btn.setIcon(QIcon(settings.get_image('user'))) self.profile_btn.setFixedSize(32, 32) self.profile_btn.clicked.connect(self.show_user_widget) self.profile_btn.setToolTip(_('View current user')) layout.addWidget(self.profile_btn, 1, 3, 1, 1) # Refresh timer update_status = int(settings.get_config('Alignak-app', 'update_status')) * 1000 self.refresh_timer.setInterval(update_status) self.refresh_timer.start() self.refresh_timer.timeout.connect(self.update_status) def show_status_dialog(self): # pragma: no cover """ Update and show StatusQDialog """ self.update_status_btn(self.status_dialog.update_dialog()) self.status_dialog.show() def show_user_widget(self): # pragma: no cover """ Update and show ProfileQWidget """ self.profile_widget.update_widget() self.profile_widget.show() def update_status_btn(self, status): """ Update status button, depending if "status_ok" is False or True :param status: current status of alignak daemons :type status: bool """ self.status_btn.setIcon( QIcon(get_icon_pixmap(status, ['connected', 'disconnected'])) ) def update_status(self): """ Update daemons and backend status """ self.backend_connected.setPixmap( QPixmap(settings.get_image(app_backend.get_backend_status_icon())) ) self.ws_connected.setPixmap( QPixmap(settings.get_image(app_backend.get_ws_status_icon())) ) self.status_btn.setEnabled(bool(data_manager.database['alignakdaemon'])) if self.status_dialog.labels: self.update_status_btn(self.status_dialog.update_dialog())
class ToolButton(QToolButton): _MultiIconOption = 1 _ShowMenuInsideOption = 2 _ToolBarLookOption = 4 _ShowMenuOnRightClick = 8 def __init__(self, parent=None): super(ToolButton, self).__init__(parent) self._multiIcon = QImage() self._themeIcon = '' self._pressTimer = QTimer() self._menu = None # QMenu self._options = 0 self.setMinimumWidth(16) opt = QStyleOptionToolButton() self.initStyleOption(opt) self._pressTimer.setSingleShot(True) self._pressTimer.setInterval(QApplication.style().styleHint( QStyle.SH_ToolButton_PopupDelay, opt, self )) self._pressTimer.timeout.connect(self._showMenu) def size(self): return super().size() def setFixedSize(self, size): return super().setFixedSize(size) fixedsize = pyqtProperty(QSize, size, setFixedSize) def width(self): return super().width() def setFixedWidth(self, width): return super().setFixedWidth(width) fixedwidth = pyqtProperty(int, width, setFixedWidth) def height(self): return super().height() def setFixedHeight(self, height): return super().setFixedHeight(height) fixedheight = pyqtProperty(int, height, setFixedHeight) def multiIcon(self): ''' @brief: MultiIcon - Image containing pixmaps for all button states @return: QImage ''' return self._multiIcon def setMultiIcon(self, image): ''' @param: image QImage ''' self._options |= self._MultiIconOption self._multiIcon = image self.setFixedSize(self._multiIcon.width(), self._multiIcon.height()) self.update() multiIcon = pyqtProperty(QImage, multiIcon, setMultiIcon) def icon(self): ''' @brief: Icon - Standard QToolButton with icon @return: QIcon ''' return super().icon() def setIcon(self, icon): ''' @param: QIcon ''' if self._options & self._MultiIconOption: self.setFixedSize(self.sizeHint()) self._options &= ~self._MultiIconOption if not isinstance(icon, QIcon): icon = QIcon(icon) super().setIcon(icon) icon = pyqtProperty(QIcon, icon, setIcon) def themeIcon(self): ''' @brief: ThemeIcon - Standard QToolButton with theme icon @return: QString ''' return self._themeIcon def setThemeIcon(self, icon): ''' @param: icon QString ''' # QIcon ic ic = QIcon.fromTheme(icon) if not ic.isNull(): self._themeIcon = icon self.setIcon(QIcon.fromTheme(self._themeIcon)) themeIcon = pyqtProperty(str, themeIcon, setThemeIcon) def fallbackIcon(self): ''' @brief: FallbackIcon - In case theme doesn't contain ThemeIcon @return: QIcon ''' return self.icon def setFallbackIcon(self, fallbackIcon): ''' @param: fallbackIcon QIcon ''' if self.icon.isNull(): self.setIcon(fallbackIcon) fallbackIcon = pyqtProperty(QIcon, fallbackIcon, setFallbackIcon) def menu(self): ''' @note: Menu - Menu is handled in ToolButton and is not passed to QToolButton There won't be menu indicator shown in the button QToolButton::MenuButtonPopup is not supported @return: QMenu ''' return self._menu def setMenu(self, menu): ''' @param: menu QMenu ''' assert(menu) if self._menu: self._menu.aboutToHide.disconnect(self._menuAboutToHide) self._menu = menu self._menu.aboutToHide.connect(self._menuAboutToHide) def showMenuInside(self): ''' @brief: Align the right corner of menu to the right corner of button ''' return self._options & self._ShowMenuInsideOption def setShowMenuInside(self, enable): if enable: self._options |= self._ShowMenuInsideOption else: self._options &= ~self._ShowMenuInsideOption def showMenuOnRightClick(self): ''' @brief: Show button menu on right click ''' return self._options & self._ShowMenuOnRightClick def setShowMenuOnRightClick(self, enable): if enable: self._options |= self._ShowMenuOnRightClick else: self._options &= ~self._ShowMenuOnRightClick def toolbarButtonLook(self): ''' @brief: Set the button to look as it was in toolbar (it now only sets the correct icon size) @return: bool ''' return self._options & self._ToolBarLookOption def setToolbarButtonLook(self, enable): if enable: self._options |= self._ToolBarLookOption opt = QStyleOption() opt.initFrom(self) size = self.style().pixelMetric(QStyle.PM_ToolBarIconSize, opt, self) self.setIconSize(QSize(size, size)) else: self._options &= ~self._ToolBarLookOption self.setProperty('toolbar-look', enable) self.style().unpolish(self) self.style().polish(self) # Q_SIGNALS middleMouseClicked = pyqtSignal() controlClicked = pyqtSignal() doubleClicked = pyqtSignal() # It is needed to use these signals with ShowMenuInside aboutToShowMenu = pyqtSignal() aboutToHideMenu = pyqtSignal() # private Q_SLOTS def _menuAboutToHide(self): self.setDown(False) self.aboutToHideMenu.emit() def _showMenu(self): if not self._menu or self._menu.isVisible(): return self.aboutToShowMenu.emit() pos = QPoint() if self._options & self._ShowMenuInsideOption: pos = self.mapToGlobal(self.rect().bottomRight()) if QApplication.layoutDirection() == Qt.RightToLeft: pos.setX(pos.x() - self.rect().width()) else: pos.setX(pos.x() - self._menu.sizeHint().width()) else: pos = self.mapToGlobal(self.rect().bottomLeft()) self._menu.popup(pos) # protected: # override def mousePressEvent(self, event): ''' @param: event QMouseEvent ''' buttons = event.buttons() if buttons == Qt.LeftButton and self.popupMode() == QToolButton.DelayedPopup: self._pressTimer.start() if buttons == Qt.LeftButton and self.menu() and self.popupMode() == QToolButton.InstantPopup: self.setDown(True) self._showMenu() elif buttons == Qt.RightButton and self.menu() and self._options & self.showMenuOnRightClick: self.setDown(True) self._showMenu() else: super().mousePressEvent(event) # override def mouseReleaseEvent(self, event): ''' @param: event QMouseEvent ''' self._pressTimer.stop() button = event.button() if button == Qt.MiddleButton and self.rect().contains(event.pos()): self.middleMouseClicked.emit() self.setDown(False) elif button == Qt.LeftButton and self.rect().contains(event.pos()) and \ event.modifiers() == Qt.ControlModifier: self.controlClicked.emit() self.setDown(False) else: super().mouseReleaseEvent(event) # override def mouseDoubleClickEvent(self, event): ''' @param: event QMouseEvent ''' super().mouseDoubleClickEvent(event) self._pressTimer.stop() if event.buttons() == Qt.LeftButton: self.doubleClicked.emit() # override def contextMenuEvent(self, event): ''' @param: event QContextMenuEvent ''' # Block to prevent showing both context menu and button menu if self.menu() and self._options & self._ShowMenuOnRightClick: return super().contextMenuEvent(event) # override def paintEvent(self, event): ''' @param: event QPaintEvent ''' if not (self._options & self._MultiIconOption): super().paintEvent(event) return p = QPainter(self) w = self._multiIcon.width() h4 = self._multiIcon.height() / 4 if not self.isEnabled(): p.drawImage(0, 0, self._multiIcon, 0, h4 * 3, w, h4) elif self.isDown(): p.drawImage(0, 0, self._multiIcon, 0, h4 * 2, w, h4) elif self.underMouse(): p.drawImage(0, 0, self._multiIcon, 0, h4 * 1, w, h4) else: p.drawImage(0, 0, self._multiIcon, 0, h4 * 0, w, h4)
class GridView(QListView): update_item = pyqtSignal(object) files_dropped = pyqtSignal(object) books_dropped = pyqtSignal(object) def __init__(self, parent): QListView.__init__(self, parent) self._ncols = None self.gesture_manager = GestureManager(self) setup_dnd_interface(self) self.setUniformItemSizes(True) self.setWrapping(True) self.setFlow(self.LeftToRight) # We cannot set layout mode to batched, because that breaks # restore_vpos() # self.setLayoutMode(self.Batched) self.setResizeMode(self.Adjust) self.setSelectionMode(self.ExtendedSelection) self.setVerticalScrollMode(self.ScrollPerPixel) self.delegate = CoverDelegate(self) self.delegate.animation.valueChanged.connect( self.animation_value_changed) self.delegate.animation.finished.connect(self.animation_done) self.setItemDelegate(self.delegate) self.setSpacing(self.delegate.spacing) self.set_color() self.ignore_render_requests = Event() dpr = self.device_pixel_ratio self.thumbnail_cache = ThumbnailCache( max_size=gprefs['cover_grid_disk_cache_size'], thumbnail_size=(int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height()))) self.render_thread = None self.update_item.connect(self.re_render, type=Qt.ConnectionType.QueuedConnection) self.doubleClicked.connect(self.double_clicked) self.setCursor(Qt.CursorShape.PointingHandCursor) self.gui = parent self.context_menu = None self.update_timer = QTimer(self) self.update_timer.setInterval(200) self.update_timer.timeout.connect(self.update_viewport) self.update_timer.setSingleShot(True) self.resize_timer = t = QTimer(self) t.setInterval(200), t.setSingleShot(True) t.timeout.connect(self.update_memory_cover_cache_size) def viewportEvent(self, ev): try: ret = self.gesture_manager.handle_event(ev) except AttributeError: ret = None if ret is not None: return ret return QListView.viewportEvent(self, ev) @property def device_pixel_ratio(self): try: return self.devicePixelRatioF() except AttributeError: return self.devicePixelRatio() @property def first_visible_row(self): geom = self.viewport().geometry() for y in range(geom.top(), (self.spacing() * 2) + geom.top(), 5): for x in range(geom.left(), (self.spacing() * 2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: return ans @property def last_visible_row(self): geom = self.viewport().geometry() for y in range(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5): for x in range(geom.left(), (self.spacing() * 2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: item_width = self.delegate.item_size.width( ) + 2 * self.spacing() return ans + (geom.width() // item_width) def update_viewport(self): self.ignore_render_requests.clear() self.update_timer.stop() m = self.model() for r in range(self.first_visible_row or 0, self.last_visible_row or (m.count() - 1)): self.update(m.index(r, 0)) def start_view_animation(self, index): d = self.delegate if d.animating is None and not config['disable_animations']: d.animating = index d.animation.start() def double_clicked(self, index): self.start_view_animation(index) if tweaks['doubleclick_on_library_view'] == 'open_viewer': self.gui.iactions['View'].view_triggered(index) elif tweaks['doubleclick_on_library_view'] in { 'edit_metadata', 'edit_cell' }: self.gui.iactions['Edit Metadata'].edit_metadata(False, False) def animation_value_changed(self, value): if self.delegate.animating is not None: self.update(self.delegate.animating) def animation_done(self): if self.delegate.animating is not None: idx = self.delegate.animating self.delegate.animating = None self.update(idx) def set_color(self): r, g, b = gprefs['cover_grid_color'] tex = gprefs['cover_grid_texture'] pal = self.palette() pal.setColor(pal.Base, QColor(r, g, b)) self.setPalette(pal) ss = '' if tex: from calibre.gui2.preferences.texture_chooser import texture_path path = texture_path(tex) if path: path = os.path.abspath(path).replace(os.sep, '/') ss += 'background-image: url({});'.format(path) ss += 'background-attachment: fixed;' pm = QPixmap(path) if not pm.isNull(): val = pm.scaled(1, 1).toImage().pixel(0, 0) r, g, b = qRed(val), qGreen(val), qBlue(val) dark = max(r, g, b) < 115 col = '#eee' if dark else '#111' ss += 'color: {};'.format(col) self.delegate.highlight_color = QColor(col) self.setStyleSheet('QListView {{ {} }}'.format(ss)) def refresh_settings(self): size_changed = ( gprefs['cover_grid_width'] != self.delegate.original_width or gprefs['cover_grid_height'] != self.delegate.original_height) if (size_changed or gprefs['cover_grid_show_title'] != self.delegate.original_show_title or gprefs['show_emblems'] != self.delegate.original_show_emblems or gprefs['emblem_size'] != self.delegate.orginal_emblem_size or gprefs['emblem_position'] != self.delegate.orginal_emblem_position): self.delegate.set_dimensions() self.setSpacing(self.delegate.spacing) if size_changed: self.delegate.cover_cache.clear() if gprefs['cover_grid_spacing'] != self.delegate.original_spacing: self.delegate.calculate_spacing() self.setSpacing(self.delegate.spacing) self.set_color() self.set_thumbnail_cache_image_size() cs = gprefs['cover_grid_disk_cache_size'] if (cs * (1024**2)) != self.thumbnail_cache.max_size: self.thumbnail_cache.set_size(cs) self.update_memory_cover_cache_size() def set_thumbnail_cache_image_size(self): dpr = self.device_pixel_ratio self.thumbnail_cache.set_thumbnail_size( int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height())) def resizeEvent(self, ev): self._ncols = None self.resize_timer.start() return QListView.resizeEvent(self, ev) def update_memory_cover_cache_size(self): try: sz = self.delegate.item_size except AttributeError: return rows, cols = self.width() // sz.width(), self.height() // sz.height() num = (rows + 1) * (cols + 1) limit = max(100, num * max(2, gprefs['cover_grid_cache_size_multiple'])) if limit != self.delegate.cover_cache.limit: self.delegate.cover_cache.set_limit(limit) def shown(self): self.update_memory_cover_cache_size() if self.render_thread is None: self.thumbnail_cache.set_database(self.gui.current_db) self.render_thread = Thread(target=self.render_covers) self.render_thread.daemon = True self.render_thread.start() def render_covers(self): q = self.delegate.render_queue while True: book_id = q.get() try: if book_id is None: return if self.ignore_render_requests.is_set(): continue try: self.render_cover(book_id) except: import traceback traceback.print_exc() finally: q.task_done() def render_cover(self, book_id): if self.ignore_render_requests.is_set(): return dpr = self.device_pixel_ratio page_width = int(dpr * self.delegate.cover_size.width()) page_height = int(dpr * self.delegate.cover_size.height()) tcdata, timestamp = self.thumbnail_cache[book_id] use_cache = False if timestamp is None: # Not in cache has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, 0) else: has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, timestamp) if has_cover and cdata is None: # The cached cover is fresh cdata = tcdata use_cache = True if has_cover: p = QImage() p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG') p.setDevicePixelRatio(dpr) if p.isNull() and cdata is tcdata: # Invalid image in cache self.thumbnail_cache.invalidate((book_id, )) self.update_item.emit(book_id) return cdata = None if p.isNull() else p if not use_cache: # cache is stale if cdata is not None: width, height = p.width(), p.height() scaled, nwidth, nheight = fit_image( width, height, page_width, page_height) if scaled: if self.ignore_render_requests.is_set(): return p = p.scaled( nwidth, nheight, Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation) p.setDevicePixelRatio(dpr) cdata = p # update cache if cdata is None: self.thumbnail_cache.invalidate((book_id, )) else: try: self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata)) except EncodeError as err: self.thumbnail_cache.invalidate((book_id, )) prints(err) except Exception: import traceback traceback.print_exc() elif tcdata is not None: # Cover was removed, but it exists in cache, remove from cache self.thumbnail_cache.invalidate((book_id, )) self.delegate.cover_cache.set(book_id, cdata) self.update_item.emit(book_id) def re_render(self, book_id): self.delegate.cover_cache.clear_staging() m = self.model() try: index = m.db.row(book_id) except (IndexError, ValueError, KeyError): return self.update(m.index(index, 0)) def shutdown(self): self.ignore_render_requests.set() self.delegate.render_queue.put(None) self.thumbnail_cache.shutdown() def set_database(self, newdb, stage=0): if stage == 0: self.ignore_render_requests.set() try: for x in (self.delegate.cover_cache, self.thumbnail_cache): self.model().db.new_api.remove_cover_cache(x) except AttributeError: pass # db is None for x in (self.delegate.cover_cache, self.thumbnail_cache): newdb.new_api.add_cover_cache(x) try: # Use a timeout so that if, for some reason, the render thread # gets stuck, we dont deadlock, future covers wont get # rendered, but this is better than a deadlock join_with_timeout(self.delegate.render_queue) except RuntimeError: print('Cover rendering thread is stuck!') finally: self.ignore_render_requests.clear() else: self.delegate.cover_cache.clear() def select_rows(self, rows): sel = QItemSelection() sm = self.selectionModel() m = self.model() # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda i_x: i_x[0] - i_x[1]): group = list(map(operator.itemgetter(1), g)) sel.merge( QItemSelection(m.index(min(group), 0), m.index(max(group), 0)), sm.Select) sm.select(sel, sm.ClearAndSelect) def selectAll(self): # We re-implement this to ensure that only indexes from column 0 are # selected. The base class implementation selects all columns. This # causes problems with selection syncing, see # https://bugs.launchpad.net/bugs/1236348 m = self.model() sm = self.selectionModel() sel = QItemSelection(m.index(0, 0), m.index(m.rowCount(QModelIndex()) - 1, 0)) sm.select(sel, sm.ClearAndSelect) def set_current_row(self, row): sm = self.selectionModel() sm.setCurrentIndex(self.model().index(row, 0), sm.NoUpdate) def set_context_menu(self, menu): self.context_menu = menu def contextMenuEvent(self, event): if self.context_menu is None: return from calibre.gui2.main_window import clone_menu m = clone_menu(self.context_menu) if islinux else self.context_menu m.popup(event.globalPos()) event.accept() def get_selected_ids(self): m = self.model() return [m.id(i) for i in self.selectionModel().selectedIndexes()] def restore_vpos(self, vpos): self.verticalScrollBar().setValue(vpos) def restore_hpos(self, hpos): pass def handle_mouse_press_event(self, ev): if QApplication.keyboardModifiers( ) & Qt.KeyboardModifier.ShiftModifier: # Shift-Click in QListView is broken. It selects extra items in # various circumstances, for example, click on some item in the # middle of a row then click on an item in the next row, all items # in the first row will be selected instead of only items after the # middle item. index = self.indexAt(ev.pos()) if not index.isValid(): return ci = self.currentIndex() sm = self.selectionModel() sm.setCurrentIndex(index, sm.NoUpdate) if not ci.isValid(): return if not sm.hasSelection(): sm.select(index, sm.ClearAndSelect) return cr = ci.row() tgt = index.row() top = self.model().index(min(cr, tgt), 0) bottom = self.model().index(max(cr, tgt), 0) sm.select(QItemSelection(top, bottom), sm.Select) else: return QListView.mousePressEvent(self, ev) def indices_for_merge(self, resolved=True): return self.selectionModel().selectedIndexes() def number_of_columns(self): # Number of columns currently visible in the grid if self._ncols is None: dpr = self.device_pixel_ratio width = int(dpr * self.delegate.cover_size.width()) height = int(dpr * self.delegate.cover_size.height()) step = max(10, self.spacing()) for y in range(step, 2 * height, step): for x in range(step, 2 * width, step): i = self.indexAt(QPoint(x, y)) if i.isValid(): for x in range(self.viewport().width() - step, self.viewport().width() - width, -step): j = self.indexAt(QPoint(x, y)) if j.isValid(): self._ncols = j.row() - i.row() + 1 return self._ncols return self._ncols def keyPressEvent(self, ev): if handle_enter_press(self, ev, self.start_view_animation, False): return k = ev.key() if ev.modifiers() & Qt.KeyboardModifier.ShiftModifier and k in ( Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down): ci = self.currentIndex() if not ci.isValid(): return c = ci.row() ncols = self.number_of_columns() or 1 delta = { Qt.Key.Key_Left: -1, Qt.Key.Key_Right: 1, Qt.Key.Key_Up: -ncols, Qt.Key.Key_Down: ncols }[k] n = max(0, min(c + delta, self.model().rowCount(None) - 1)) if n == c: return sm = self.selectionModel() rows = {i.row() for i in sm.selectedIndexes()} if rows: mi, ma = min(rows), max(rows) end = mi if c == ma else ma if c == mi else c else: end = c top = self.model().index(min(n, end), 0) bottom = self.model().index(max(n, end), 0) sm.select(QItemSelection(top, bottom), sm.ClearAndSelect) sm.setCurrentIndex(self.model().index(n, 0), sm.NoUpdate) else: return QListView.keyPressEvent(self, ev) @property def current_book(self): ci = self.currentIndex() if ci.isValid(): try: return self.model().db.data.index_to_id(ci.row()) except (IndexError, ValueError, KeyError, TypeError, AttributeError): pass def current_book_state(self): return self.current_book def restore_current_book_state(self, state): book_id = state self.setFocus(Qt.FocusReason.OtherFocusReason) try: row = self.model().db.data.id_to_index(book_id) except (IndexError, ValueError, KeyError, TypeError, AttributeError): return self.set_current_row(row) self.select_rows((row, )) self.scrollTo(self.model().index(row, 0), self.PositionAtCenter) def marked_changed(self, old_marked, current_marked): changed = old_marked | current_marked m = self.model() for book_id in changed: try: self.update(m.index(m.db.data.id_to_index(book_id), 0)) except ValueError: pass def moveCursor(self, action, modifiers): index = QListView.moveCursor(self, action, modifiers) if action in (QListView.MoveLeft, QListView.MoveRight) and index.isValid(): ci = self.currentIndex() if ci.isValid() and index.row() == ci.row(): nr = index.row() + (1 if action == QListView.MoveRight else -1) if 0 <= nr < self.model().rowCount(QModelIndex()): index = self.model().index(nr, 0) return index def selectionCommand(self, index, event): if event and event.type() == event.KeyPress and event.key() in ( Qt.Key.Key_Home, Qt.Key.Key_End) and event.modifiers() & Qt.Modifier.CTRL: return QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows return super(GridView, self).selectionCommand(index, event) def wheelEvent(self, ev): if ev.phase() not in (Qt.ScrollPhase.ScrollUpdate, 0, Qt.ScrollPhase.ScrollMomentum): return number_of_pixels = ev.pixelDelta() number_of_degrees = ev.angleDelta() / 8.0 b = self.verticalScrollBar() if number_of_pixels.isNull() or islinux: # pixelDelta() is broken on linux with wheel mice dy = number_of_degrees.y() / 15.0 # Scroll by approximately half a row dy = int(math.ceil((dy) * b.singleStep() / 2.0)) else: dy = number_of_pixels.y() if abs(dy) > 0: b.setValue(b.value() - dy) def paintEvent(self, ev): dpr = self.device_pixel_ratio page_width = int(dpr * self.delegate.cover_size.width()) page_height = int(dpr * self.delegate.cover_size.height()) size_changed = self.thumbnail_cache.set_thumbnail_size( page_width, page_height) if size_changed: self.delegate.cover_cache.clear() return super(GridView, self).paintEvent(ev)
class LocationCompleterView(QWidget): def __init__(self): super().__init__(None) self._view = None # QListView self._delegate = None # LocationCompleterDelegate self._searchEnginesLayout = None # QHBoxLayout self._resizeHeight = -1 self._resizeTimer = None # QTimer self._forceResize = True self.setAttribute(Qt.WA_ShowWithoutActivating) self.setAttribute(Qt.WA_X11NetWmWindowTypeCombo) if gVar.app.platformName() == 'xcb': self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint | Qt.BypassWindowManagerHint) else: self.setWindowFlags(Qt.Popup) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self._view = QListView(self) layout.addWidget(self._view) self._view.setUniformItemSizes(True) self._view.setEditTriggers(QAbstractItemView.NoEditTriggers) self._view.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self._view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._view.setSelectionBehavior(QAbstractItemView.SelectRows) self._view.setSelectionMode(QAbstractItemView.SingleSelection) self._view.setMouseTracking(True) gVar.app.installEventFilter(self) self._delegate = LocationCompleterDelegate(self) self._view.setItemDelegate(self._delegate) searchFrame = QFrame(self) searchFrame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised) searchLayout = QHBoxLayout(searchFrame) searchLayout.setContentsMargins(10, 4, 4, 4) searchSettingsButton = ToolButton(self) searchSettingsButton.setIcon(IconProvider.settingsIcon()) searchSettingsButton.setToolTip(_('Manage Search Engines')) searchSettingsButton.setAutoRaise(True) searchSettingsButton.setIconSize(QSize(16, 16)) searchSettingsButton.clicked.connect(self.searchEnginesDialogRequested) searchLabel = QLabel(_('Search with:')) self._searchEnginesLayout = QHBoxLayout() self._setupSearchEngines() gVar.app.searchEnginesManager().enginesChanged.connect( self._setupSearchEngines) searchLayout.addWidget(searchLabel) searchLayout.addLayout(self._searchEnginesLayout) searchLayout.addStretch() searchLayout.addWidget(searchSettingsButton) layout.addWidget(searchFrame) def model(self): ''' @return: QAbstractItemModel ''' return self._view.model() def setModel(self, model): ''' @param model QAbstractItemModel ''' self._view.setModel(model) def selectionModel(self): ''' @return: QItemSelectionModel ''' return self._view.selectionModel() def currentIndex(self): ''' @return: QModelIndex ''' return self._view.currentIndex() def setCurrentIndex(self, index): ''' @param index QModelIndex ''' self._view.setCurrentIndex(index) def adjustSize(self): maxItemsCount = 12 newHeight = self._view.sizeHintForRow(0) * min(maxItemsCount, self.model().rowCount()) if not self._resizeTimer: self._resizeTimer = QTimer(self) self._resizeTimer.setInterval(200) def func(): if self._resizeHeight > 0: self._view.setFixedHeight(self._resizeHeight) self.setFixedHeight(self.sizeHint().height()) self._resizeHeight = -1 self._resizeTimer.timeout.connect(func) if not self._forceResize: if newHeight == self._resizeHeight: return elif newHeight == self._view.height(): self._resizeHeight = -1 return elif newHeight < self._view.height(): self._resizeHeight = newHeight self._resizeTimer.start() return self._resizeHeight = -1 self._forceResize = False self._view.setFixedHeight(newHeight) self.setFixedHeight(self.sizeHint().height()) # override def eventFilter(self, obj, event): # noqa C901 ''' @param obj QObject @param event QEvent @return: bool ''' # Event filter based on QCompleter::eventFilter from qcompleter.cpp if obj == self or obj == self._view or not self.isVisible(): return False evtType = event.type() if obj == self._view.viewport(): if evtType == QEvent.MouseButtonRelease: # QMouseEvent e = event index = self._view.indexAt(e.pos()) if not index.isValid(): return False # Qt::MouseButton button = e.button() # Qt::KeyboardModifiers modifiers = e.modifiers() if button == Qt.LeftButton and modifiers == Qt.NoModifier: self.indexActivated.emit(index) return True if button == Qt.MiddleButton or (button == Qt.LeftButton and modifiers == Qt.ControlModifier): self.indexCtrlActivated.emit(index) return True if button == Qt.LeftButton and modifiers == Qt.ShiftModifier: self.indexShiftActivated.emit(index) return True return False if evtType == QEvent.KeyPress: # QKeyEvent keyEvent = event evtKey = keyEvent.key() modifiers = keyEvent.modifiers() index = self._view.currentIndex() item = self.model().index(0, 0) if item.data(LocationCompleterModel.VisitSearchItemRole): visitSearchIndex = item else: visitSearchIndex = QModelIndex() if (evtKey == Qt.Key_Up or evtKey == Qt.Key_Down) and \ self._view.currentIndex() != index: self._view.setCurrentIndex(index) # TODO: ? if evtKey in (Qt.Key_Return, Qt.Key_Enter): if index.isValid(): if modifiers == Qt.NoModifier or modifiers == Qt.KeypadModifier: self.indexActivated.emit(index) return True if modifiers == Qt.ControlModifier: self.indexCtrlActivated.emit(index) return True if modifiers == Qt.ShiftModifier: self.indexShiftActivated.emit(index) return True elif evtKey == Qt.Key_End: if modifiers & Qt.ControlModifier: self._view.setCurrentIndex(self.model().index( self.model().rowCount() - 1, 0)) return True else: self.close() elif evtKey == Qt.Key_Home: if modifiers & Qt.ControlModifier: self._view.setCurrentIndex(self.model().index(0, 0)) self._view.scrollToTop() return True else: self.close() elif evtKey == Qt.Key_Escape: self.close() return True elif evtKey == Qt.Key_F4: if modifiers == Qt.AltModifier: self.close() return False elif evtKey in (Qt.Key_Tab, Qt.Key_Backtab): if modifiers != Qt.NoModifier and modifiers != Qt.ShiftModifier: return False isBack = evtKey == Qt.Key_Backtab if evtKey == Qt.Key_Tab and modifiers == Qt.ShiftModifier: isBack = True ev = QKeyEvent(QKeyEvent.KeyPress, isBack and Qt.Key_Up or Qt.Key_Down, Qt.NoModifier) QApplication.sendEvent(self.focusProxy(), ev) return True elif evtKey in (Qt.Key_Up, Qt.Key_PageUp): if modifiers != Qt.NoModifier: return False step = evtKey == Qt.Key_PageUp and 5 or 1 if not index.isValid() or index == visitSearchIndex: rowCount = self.model().rowCount() lastIndex = self.model().index(rowCount - 1, 0) self._view.setCurrentIndex(lastIndex) elif index.row() == 0: self._view.setCurrentIndex(QModelIndex()) else: row = max(0, index.row() - step) self._view.setCurrentIndex(self.model().index(row, 0)) return True elif evtKey in (Qt.Key_Down, Qt.Key_PageDown): if modifiers != Qt.NoModifier: return False step = evtKey == Qt.Key_PageDown and 5 or 1 if not index.isValid(): firstIndex = self.model().index(0, 0) self._view.setCurrentIndex(firstIndex) elif index != visitSearchIndex and index.row( ) == self.model().rowCount() - 1: self._view.setCurrentIndex(visitSearchIndex) self._view.scrollToTop() else: row = min(self.model().rowCount() - 1, index.row() + step) self._view.setCurrentIndex(self.model().index(row, 0)) return True elif evtKey == Qt.Key_Delete: if index != visitSearchIndex and self._view.viewport().rect( ).contains(self._view.visualRect(index)): self.indexDeleteRequested.emit(index) return True elif evtKey == Qt.Key_Shift: self._delegate.setForceVisitItem(True) self._view.viewport().update() # end of switch evtKey if self.focusProxy(): self.focusProxy().event(keyEvent) return True elif evtType == QEvent.KeyRelease: if event.key() == Qt.Key_Shift: self._delegate.setForceVisitItem(False) self._view.viewport().update() return True elif evtType in (QEvent.Wheel, QEvent.MouseButtonPress): if not self.underMouse(): self.close() return False elif evtType == QEvent.FocusOut: # QFocusEvent focusEvent = event reason = focusEvent.reason() if reason != Qt.PopupFocusReason and reason != Qt.MouseFocusReason: self.close() elif evtType in (QEvent.Move, QEvent.Resize): w = obj if isinstance(w, QWidget) and w.isWindow() and self.focusProxy( ) and w == self.focusProxy().window(): self.close() # end of switch evtType return False # Q_SIGNALS closed = pyqtSignal() searchEnginesDialogRequested = pyqtSignal() loadRequested = pyqtSignal(LoadRequest) indexActivated = pyqtSignal(QModelIndex) indexCtrlActivated = pyqtSignal(QModelIndex) indexShiftActivated = pyqtSignal(QModelIndex) indexDeleteRequested = pyqtSignal(QModelIndex) # public Q_SLOTS: def close(self): self.hide() self._view.verticalScrollBar().setValue(0) self._delegate.setForceVisitItem(False) self._forceResize = True self.closed.emit() # private: def _setupSearchEngines(self): for idx in (range(self._searchEnginesLayout.count())): item = self._searchEnginesLayout.takeAt(0) item.deleteLater() engines = gVar.app.searchEnginesManager().allEngines() for engine in engines: button = ToolButton(self) button.setIcon(engine.icon) button.setToolTip(engine.name) button.setAutoRaise(True) button.setIconSize(QSize(16, 16)) def func(): text = self.model().index(0, 0).data( LocationCompleterModel.SearchStringRole) self.loadRequested.emit( gVar.app.searchEngineManager().searchResult(engine, text)) button.clicked.connect(func) self._searchEnginesLayout.addWidget(button)
class Splitter(QSplitter): state_changed = pyqtSignal(object) def __init__(self, name, label, icon, initial_show=True, initial_side_size=120, connect_button=True, orientation=Qt.Horizontal, side_index=0, parent=None, shortcut=None): QSplitter.__init__(self, parent) self.resize_timer = QTimer(self) self.resize_timer.setSingleShot(True) self.desired_side_size = initial_side_size self.desired_show = initial_show self.resize_timer.setInterval(5) self.resize_timer.timeout.connect(self.do_resize) self.setOrientation(orientation) self.side_index = side_index self._name = name self.label = label self.initial_side_size = initial_side_size self.initial_show = initial_show self.splitterMoved.connect(self.splitter_moved, type=Qt.QueuedConnection) self.button = LayoutButton(icon, label, self, shortcut=shortcut) if connect_button: self.button.clicked.connect(self.double_clicked) if shortcut is not None: self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label, self) self.action_toggle.triggered.connect(self.toggle_triggered) if parent is not None: parent.addAction(self.action_toggle) if hasattr(parent, 'keyboard'): parent.keyboard.register_shortcut( 'splitter %s %s' % (name, label), unicode(self.action_toggle.text()), default_keys=(shortcut, ), action=self.action_toggle) else: self.action_toggle.setShortcut(shortcut) else: self.action_toggle.setShortcut(shortcut) def toggle_triggered(self, *args): self.toggle_side_pane() def createHandle(self): return SplitterHandle(self.orientation(), self) def initialize(self): for i in range(self.count()): h = self.handle(i) if h is not None: h.splitter_moved() self.state_changed.emit(not self.is_side_index_hidden) def splitter_moved(self, *args): self.desired_side_size = self.side_index_size self.state_changed.emit(not self.is_side_index_hidden) @property def is_side_index_hidden(self): sizes = list(self.sizes()) try: return sizes[self.side_index] == 0 except IndexError: return True @property def save_name(self): ori = 'horizontal' if self.orientation() == Qt.Horizontal \ else 'vertical' return self._name + '_' + ori def print_sizes(self): if self.count() > 1: print self.save_name, 'side:', self.side_index_size, 'other:', print list(self.sizes())[self.other_index] @dynamic_property def side_index_size(self): def fget(self): if self.count() < 2: return 0 return self.sizes()[self.side_index] def fset(self, val): if self.count() < 2: return if val == 0 and not self.is_side_index_hidden: self.save_state() sizes = list(self.sizes()) for i in range(len(sizes)): sizes[i] = val if i == self.side_index else 10 self.setSizes(sizes) total = sum(self.sizes()) sizes = list(self.sizes()) for i in range(len(sizes)): sizes[i] = val if i == self.side_index else total - val self.setSizes(sizes) self.initialize() return property(fget=fget, fset=fset) def do_resize(self, *args): orig = self.desired_side_size QSplitter.resizeEvent(self, self._resize_ev) if orig > 20 and self.desired_show: c = 0 while abs(self.side_index_size - orig) > 10 and c < 5: self.apply_state(self.get_state(), save_desired=False) c += 1 def resizeEvent(self, ev): if self.resize_timer.isActive(): self.resize_timer.stop() self._resize_ev = ev self.resize_timer.start() def get_state(self): if self.count() < 2: return (False, 200) return (self.desired_show, self.desired_side_size) def apply_state(self, state, save_desired=True): if state[0]: self.side_index_size = state[1] if save_desired: self.desired_side_size = self.side_index_size else: self.side_index_size = 0 self.desired_show = state[0] def default_state(self): return (self.initial_show, self.initial_side_size) # Public API {{{ def update_desired_state(self): self.desired_show = not self.is_side_index_hidden def save_state(self): if self.count() > 1: gprefs[self.save_name + '_state'] = self.get_state() @property def other_index(self): return (self.side_index + 1) % 2 def restore_state(self): if self.count() > 1: state = gprefs.get(self.save_name + '_state', self.default_state()) self.apply_state(state, save_desired=False) self.desired_side_size = state[1] def toggle_side_pane(self, hide=None): if hide is None: action = 'show' if self.is_side_index_hidden else 'hide' else: action = 'hide' if hide else 'show' getattr(self, action + '_side_pane')() def show_side_pane(self): if self.count() < 2 or not self.is_side_index_hidden: return if self.desired_side_size == 0: self.desired_side_size = self.initial_side_size self.apply_state((True, self.desired_side_size)) def hide_side_pane(self): if self.count() < 2 or self.is_side_index_hidden: return self.apply_state((False, self.desired_side_size)) def double_clicked(self, *args): self.toggle_side_pane()
class DeviceBar(QWidget): """ DeviceBar Signals: onDeviceUpdated() onDeviceSelected(str) # str = id onDeviceChanged(str) # str = id """ onDeviceUpdated = pyqtSignal(str, name="onDeviceUpdated") onDeviceSelected = pyqtSignal(str, name="onDeviceSelected") onDeviceChanged = pyqtSignal(str, name="onDeviceChanged") def __init__(self, parent=None, device_type='usb'): super().__init__(parent=parent) # dont show for local if device_type != 'usb': return self.parent = parent self.wait_for_devtype = device_type self.is_waiting = True self._adb = Adb() if not self._adb.min_required: raise Exception('Adb missing or no Device') self._git = Git() self.setAutoFillBackground(True) self.setStyleSheet( 'background-color: crimson; color: white; font-weight: bold; margin: 0; padding: 10px;' ) self.setup() self._timer = QTimer() self._timer.setInterval(500) self._timer.timeout.connect(self._on_timer) self._timer.start() self._timer_step = 0 frida.get_device_manager().on('added', self._on_device) frida.get_device_manager().on('removed', self._on_device) self.devices_thread = DevicesUpdateThread(self) self.devices_thread.onAddDevice.connect(self.on_add_deviceitem) self.devices_thread.onDevicesUpdated.connect(self._on_devices_finished) self._update_thread = FridaUpdateThread(self) self._update_thread._adb = self._adb self._update_thread.onStatusUpdate.connect(self._update_statuslbl) self._update_thread.onFinished.connect(self._frida_updated) self._update_thread.onError.connect(self._on_download_error) self.updated_frida_version = '' self.updated_frida_assets_url = {} self._device_id = None self._devices = [] remote_frida = self._git.get_frida_version() if remote_frida is None: self.updated_frida_version = '' self.updated_frida_assets_url.clear() else: self.updated_frida_version = remote_frida['tag_name'] for asset in remote_frida['assets']: if 'name' not in asset: continue asset_name = asset['name'] if not asset_name.startswith('frida-server-'): continue if not 'android' in asset_name: continue tag_start = asset_name.index('android-') if asset_name.index('server') >= 0: tag = asset_name[tag_start + 8:-3] self.updated_frida_assets_url[tag] = asset[ 'browser_download_url'] def setup(self): """ Setup ui """ h_box = QHBoxLayout() h_box.setContentsMargins(0, 0, 0, 0) self.update_label = QLabel('Waiting for Device') self.update_label.setFixedWidth(self.parent.width()) self.update_label.setOpenExternalLinks(True) self.update_label.setTextFormat(Qt.RichText) self.update_label.setFixedHeight(35) self.update_label.setTextInteractionFlags(Qt.TextBrowserInteraction) self._install_btn = QPushButton('Install Frida', self.update_label) self._install_btn.setStyleSheet('padding: 0; border-color: white;') self._install_btn.setGeometry(self.update_label.width() - 110, 5, 100, 25) self._install_btn.clicked.connect(self._on_install_btn) self._install_btn.setVisible(False) self._start_btn = QPushButton('Start Frida', self.update_label) self._start_btn.setStyleSheet('padding: 0; border-color: white;') self._start_btn.setGeometry(self.update_label.width() - 110, 5, 100, 25) self._start_btn.clicked.connect(self._on_start_btn) self._start_btn.setVisible(False) self._update_btn = QPushButton('Update Frida', self.update_label) self._update_btn.setStyleSheet('padding: 0; border-color: white;') self._update_btn.setGeometry(self.update_label.width() - 110, 5, 100, 25) self._update_btn.clicked.connect(self._on_install_btn) self._update_btn.setVisible(False) self._restart_btn = QPushButton('Restart Frida', self.update_label) self._restart_btn.setStyleSheet('padding: 0; border-color: white;') self._restart_btn.setGeometry(self.update_label.width() - 110, 5, 100, 25) self._restart_btn.clicked.connect(self._on_restart_btn) self._restart_btn.setVisible(False) self._devices_combobox = QComboBox(self.update_label) self._devices_combobox.setStyleSheet( 'padding: 2px 5px; border-color: white;') self._devices_combobox.setGeometry(self.update_label.width() - 320, 5, 200, 25) self._devices_combobox.currentIndexChanged.connect( self._on_device_changed) self._devices_combobox.setVisible(False) h_box.addWidget(self.update_label) self.setLayout(h_box) def on_add_deviceitem(self, device_ident): """ Adds an Item to the DeviceComboBox """ if device_ident['type'] == self.wait_for_devtype: if device_ident['name'] not in self._devices: self._devices.append(device_ident) self._timer_step = -1 self.is_waiting = False def _on_device_changed(self, index): device = None device_id = self._devices_combobox.itemData(index) if device_id: try: device = frida.get_device(device_id) except: return if device: self._device_id = device.id self._check_device(device) self.onDeviceChanged.emit(self._device_id) def _check_device(self, frida_device): self.update_label.setStyleSheet('background-color: crimson;') self._install_btn.setVisible(False) self._update_btn.setVisible(False) self._start_btn.setVisible(False) self._restart_btn.setVisible(False) self._adb.device = frida_device.id self._device_id = frida_device.id if self._adb.available(): self.update_label.setText('Device: ' + frida_device.name) # try getting frida version device_frida = self._adb.get_frida_version() # frida not found show install button if device_frida is None: self._install_btn.setVisible(True) else: # frida is old show update button if self.updated_frida_version != device_frida: self._start_btn.setVisible(True) self._update_btn.setVisible(False) # old frida is running allow use of this version if self._adb.is_frida_running(): self._start_btn.setVisible(False) if self.updated_frida_assets_url: self._update_btn.setVisible(True) self.update_label.setStyleSheet( 'background-color: yellowgreen;') self.onDeviceUpdated.emit(frida_device.id) # frida not running show start button elif device_frida and not self._adb.is_frida_running(): self._start_btn.setVisible(True) # frida is running with last version show restart button elif device_frida and self._adb.is_frida_running(): self.update_label.setStyleSheet( 'background-color: yellowgreen;') self._restart_btn.setVisible(True) self.onDeviceUpdated.emit(frida_device.id) elif self._adb.non_root_available(): self.update_label.setText('Device: ' + frida_device.name + ' (NOROOT!)') self.onDeviceUpdated.emit(frida_device.id) def _on_devices_finished(self): if self._devices: if len(self._devices) > 1: self._devices_combobox.clear() self._devices_combobox.setVisible(True) self.update_label.setText('Please select the Device: ') for device in self._devices: self._devices_combobox.addItem(device['name'], device['id']) else: self._devices_combobox.setVisible(False) try: device = frida.get_device(self._devices[0]['id']) self._check_device(device) except: pass def _on_timer(self): if self._timer_step == -1: self._timer.stop() return if self._timer_step == 0: self.update_label.setText(self.update_label.text() + ' .') self._timer_step = 1 elif self._timer_step == 1: self.update_label.setText(self.update_label.text() + '.') self._timer_step = 2 elif self._timer_step == 2: self.update_label.setText(self.update_label.text() + '.') self._timer_step = 3 else: self.update_label.setText( self.update_label.text()[:-self._timer_step]) self._timer_step = 0 if self.is_waiting and self.devices_thread is not None: if not self.devices_thread.isRunning(): self.devices_thread.start() def _on_download_error(self, text): self._timer_step = -1 self.update_label.setStyleSheet('background-color: crimson;') self.update_label.setText(text) self._install_btn.setVisible(True) self._update_btn.setVisible(False) def _on_device(self): self.update_label.setText('Waiting for Device ...') self._timer_step = 3 self.is_waiting = True self._on_timer() def _on_install_btn(self): # urls are empty if not self.updated_frida_assets_url: return arch = self._adb.get_device_arch() request_url = '' if arch is not None and len(arch) > 1: arch = arch.join(arch.split()) if arch == 'arm64' or arch == 'arm64-v8a': request_url = self.updated_frida_assets_url['arm64'] elif arch == 'armeabi-v7a': request_url = self.updated_frida_assets_url['arm'] else: if arch in self.updated_frida_assets_url: request_url = self.updated_frida_assets_url[arch] try: if self._adb.available() and request_url.index( 'https://') == 0: self._install_btn.setVisible(False) self._update_btn.setVisible(False) qApp.processEvents() if self._update_thread is not None: if not self._update_thread.isRunning(): self._update_thread.frida_update_url = request_url self._update_thread.adb = self._adb self._update_thread.start() except ValueError: # something wrong in .git_cache folder print("request_url not set") def _update_statuslbl(self, text): self._timer.stop() self._timer_step = 0 self._timer.start() self.update_label.setText(text) def _frida_updated(self): # self._timer_step = 3 # self.is_waiting = True self._on_devices_finished() def _on_start_btn(self): if self._adb.available(): self._start_btn.setVisible(False) qApp.processEvents() if self._adb.start_frida(): # self.onDeviceUpdated.emit(self._device_id) self._on_devices_finished() else: self._start_btn.setVisible(True) def _on_restart_btn(self): if self._adb.available(): self._restart_btn.setVisible(False) qApp.processEvents() if self._adb.start_frida(restart=True): self._restart_btn.setVisible(True) # self.onDeviceUpdated.emit(self._device_id) self._on_devices_finished()
class DashboardQWidget(QWidget): """ Class who manage Host and Services resume QWidgets with number of: * Hosts: ``UP``, ``UNREACHABLE``, ``DOWN`` * Services: ``OK``, ``WARNING``, ``CRITICAL``, ``UNKNWON``, ``UNREACHABLE`` * Hosts and services: ``NOT_MONITORED``, ``ACKNOWLEDGED``, ``DOWNTIMED`` """ def __init__(self, parent=None): super(DashboardQWidget, self).__init__(parent) # Fields self.layout = QGridLayout() self.items_nb = { 'hosts_nb': QLabel(), 'services_nb': QLabel() } self.hosts_labels = { 'hosts_up': QLabel(), 'hosts_unreachable': QLabel(), 'hosts_down': QLabel(), 'hosts_not_monitored': QLabel(), 'acknowledge': QLabel(), 'downtime': QLabel() } self.services_labels = { 'services_ok': QLabel(), 'services_warning': QLabel(), 'services_critical': QLabel(), 'services_unknown': QLabel(), 'services_unreachable': QLabel(), 'services_not_monitored': QLabel(), 'acknowledge': QLabel(), 'downtime': QLabel() } self.hosts_buttons = { 'hosts_up': QPushButton(), 'hosts_unreachable': QPushButton(), 'hosts_down': QPushButton(), 'hosts_not_monitored': QPushButton(), 'acknowledge': QPushButton(), 'downtime': QPushButton() } self.services_buttons = { 'services_ok': QPushButton(), 'services_warning': QPushButton(), 'services_critical': QPushButton(), 'services_unknown': QPushButton(), 'services_unreachable': QPushButton(), 'services_not_monitored': QPushButton(), 'acknowledge': QPushButton(), 'downtime': QPushButton() } self.refresh_timer = QTimer() self.setFixedHeight(85) def initialize(self): """ Initialize QWidget """ self.setLayout(self.layout) self.layout.setContentsMargins(0, 0, 0, 0) self.get_host_resume_widget() self.get_services_resume_widget() self.update_dashboard() update_dashboard = int(settings.get_config('Alignak-app', 'update_dashboard')) * 1000 self.refresh_timer.setInterval(update_dashboard) self.refresh_timer.start() self.refresh_timer.timeout.connect(self.update_dashboard) def get_host_resume_widget(self): """ Return Host resume QWidget """ self.layout.addWidget(QLabel(_('<b>Hosts:</b>')), 0, 0, 1, 1) self.items_nb['hosts_nb'].setObjectName('subtitle') self.layout.addWidget(self.items_nb['hosts_nb'], 1, 0, 1, 1) row = 1 for icon in Host.get_available_icons(): self.hosts_buttons[icon].setIcon(QIcon(settings.get_image(icon))) self.hosts_buttons[icon].setFixedSize(48, 24) self.hosts_buttons[icon].setObjectName(icon) self.hosts_buttons[icon].setToolTip( _('Hosts %s. See in WebUI ?') % icon.replace('hosts_', '').upper() ) self.hosts_buttons[icon].clicked.connect( lambda: self.open_item_type_url('hosts') ) self.layout.addWidget(self.hosts_buttons[icon], 0, row, 1, 1) self.layout.setAlignment(self.hosts_buttons[icon], Qt.AlignCenter) self.hosts_labels[icon].setObjectName(icon) self.layout.addWidget(self.hosts_labels[icon], 1, row, 1, 1) self.layout.setAlignment(self.hosts_labels[icon], Qt.AlignCenter) row += 1 def get_services_resume_widget(self): """ Return Services resume QWidget """ self.layout.addWidget(QLabel('<b>Services:</b>'), 2, 0, 1, 1) self.items_nb['services_nb'].setObjectName('subtitle') self.layout.addWidget(self.items_nb['services_nb'], 3, 0, 1, 1) row = 1 for icon in Service.get_available_icons(): self.services_buttons[icon].setIcon(QIcon(settings.get_image(icon))) self.services_buttons[icon].setFixedSize(48, 24) self.services_buttons[icon].setObjectName(icon) self.services_buttons[icon].setToolTip( _('Services %s. See in WebUI ?') % icon.replace('services_', '').upper() ) self.services_buttons[icon].clicked.connect( lambda: self.open_item_type_url('services') ) self.layout.addWidget(self.services_buttons[icon], 2, row, 1, 1) self.layout.setAlignment(self.services_buttons[icon], Qt.AlignCenter) self.services_labels[icon].setObjectName(icon) self.layout.addWidget(self.services_labels[icon], 3, row, 1, 1) self.layout.setAlignment(self.services_labels[icon], Qt.AlignCenter) row += 1 def open_item_type_url(self, item_type): """ Retrieve sender to send right endpoint to open_url() function for item type :param item_type: type of item: hosts | services :type item_type: str """ endpoint = self.sender().objectName() open_url('%s%s' % (item_type, get_url_endpoint_from_icon_name(endpoint))) def update_dashboard(self): """ Update number of items in dashboard """ synthesis = data_manager.get_synthesis_count() hosts_sum = 0 for item in synthesis['hosts']: hosts_sum += synthesis['hosts'][item] services_sum = 0 for item in synthesis['services']: services_sum += synthesis['services'][item] # Hosts percentages self.items_nb['hosts_nb'].setText("%d hosts" % hosts_sum) for icon in Host.get_available_icons(): host_nb = synthesis['hosts'][icon.replace('hosts_', '')] percent = 0.0 try: percent = float(host_nb) * 100.0 / float(hosts_sum) except ZeroDivisionError: pass item_text = '%d (%.02f%%)' % (host_nb, percent) self.hosts_labels[icon].setText(item_text) # Services percentage self.items_nb['services_nb'].setText("%d services" % services_sum) for icon in Service.get_available_icons(): service_nb = synthesis['services'][icon.replace('services_', '')] percent = 0.0 try: percent = float(service_nb) * 100.0 / float(services_sum) except ZeroDivisionError: pass item_text = '%d (%.01f%%)' % (service_nb, percent) self.services_labels[icon].setText(item_text) for button in self.hosts_buttons: if settings.get_config('Alignak', 'webui'): self.hosts_buttons[button].setEnabled(True) self.hosts_buttons[button].setToolTip( _('Hosts %s. See in WebUI ?') % button.replace('hosts_', '').upper() ) else: self.hosts_buttons[button].setToolTip( _("Hosts %s. WebUI is not set in configuration file.") % button.replace( 'hosts_', '').upper() ) for button in self.services_buttons: if settings.get_config('Alignak', 'webui'): self.services_buttons[button].setEnabled(True) self.services_buttons[button].setToolTip( _('Services %s. See in WebUI ?') % button.replace('services_', '').upper() ) else: self.services_buttons[button].setToolTip( _("Services %s. WebUI is not set in configuration file.") % button.replace( 'services_', '').upper() )
class LiveCSS(QWidget): goto_declaration = pyqtSignal(object) def __init__(self, preview, parent=None): QWidget.__init__(self, parent) self.preview = preview self.preview_is_refreshing = False self.refresh_needed = False preview.refresh_starting.connect(self.preview_refresh_starting) preview.refreshed.connect(self.preview_refreshed) self.apply_theme() self.setAutoFillBackground(True) self.update_timer = QTimer(self) self.update_timer.timeout.connect(self.update_data) self.update_timer.setSingleShot(True) self.update_timer.setInterval(500) self.now_showing = (None, None, None) self.stack = s = QStackedLayout(self) self.setLayout(s) self.clear_label = la = QLabel( "<h3>" + _("No style information found") + "</h3><p>" + _("Move the cursor inside a HTML tag to see what styles" " apply to that tag.") ) la.setWordWrap(True) la.setAlignment(Qt.AlignTop | Qt.AlignLeft) s.addWidget(la) self.box = box = Box(self) box.hyperlink_activated.connect(self.goto_declaration, type=Qt.QueuedConnection) self.scroll = sc = QScrollArea(self) sc.setWidget(box) sc.setWidgetResizable(True) s.addWidget(sc) def preview_refresh_starting(self): self.preview_is_refreshing = True def preview_refreshed(self): self.preview_is_refreshing = False # We must let the event loop run otherwise the webview will return # stale data in read_data() self.refresh_needed = True self.start_update_timer() def apply_theme(self): f = self.font() f.setFamily(tprefs["editor_font_family"] or default_font_family()) f.setPointSize(tprefs["editor_font_size"]) self.setFont(f) theme = get_theme(tprefs["editor_theme"]) pal = self.palette() pal.setColor(pal.Window, theme_color(theme, "Normal", "bg")) pal.setColor(pal.WindowText, theme_color(theme, "Normal", "fg")) pal.setColor(pal.AlternateBase, theme_color(theme, "HighlightRegion", "bg")) pal.setColor(pal.Link, theme_color(theme, "Link", "fg")) pal.setColor(pal.LinkVisited, theme_color(theme, "Keyword", "fg")) self.setPalette(pal) if hasattr(self, "box"): self.box.relayout() self.update() def clear(self): self.stack.setCurrentIndex(0) def show_data(self, editor_name, sourceline, tags): if self.preview_is_refreshing: return if sourceline is None: self.clear() else: data = self.read_data(sourceline, tags) if data is None or len(data["computed_css"]) < 1: if editor_name == self.current_name and (editor_name, sourceline, tags) == self.now_showing: # Try again in a little while in case there was a transient # error in the web view self.start_update_timer() return if self.now_showing == (None, None, None) or self.now_showing[0] != self.current_name: self.clear() return # Try to refresh the data for the currently shown tag instead # of clearing editor_name, sourceline, tags = self.now_showing data = self.read_data(sourceline, tags) if data is None or len(data["computed_css"]) < 1: self.clear() return self.now_showing = (editor_name, sourceline, tags) data["html_name"] = editor_name self.box.show_data(data) self.refresh_needed = False self.stack.setCurrentIndex(1) def read_data(self, sourceline, tags): mf = self.preview.view.page().mainFrame() tags = [x.lower() for x in tags] result = unicode( mf.evaluateJavaScript( "window.calibre_preview_integration.live_css(%s, %s)" % (json.dumps(sourceline), json.dumps(tags)) ) or "" ) try: result = json.loads(result) except ValueError: result = None if result is not None: maximum_specificities = {} for node in result["nodes"]: is_ancestor = node["is_ancestor"] for rule in node["css"]: self.process_rule(rule, is_ancestor, maximum_specificities) for node in result["nodes"]: for rule in node["css"]: for prop in rule["properties"]: if prop.specificity < maximum_specificities[prop.name]: prop.is_overriden = True return result def process_rule(self, rule, is_ancestor, maximum_specificities): selector = rule["selector"] sheet_index = rule["sheet_index"] rule_address = rule["rule_address"] or () if selector is not None: try: specificity = [0] + list(parse(selector)[0].specificity()) except (AttributeError, TypeError): specificity = [0, 0, 0, 0] else: # style attribute specificity = [1, 0, 0, 0] specificity.extend((sheet_index, tuple(rule_address))) ancestor_specificity = 0 if is_ancestor else 1 properties = [] for prop in rule["properties"]: important = 1 if prop[-1] == "important" else 0 p = Property(prop, [ancestor_specificity] + [important] + specificity) properties.append(p) if p.specificity > maximum_specificities.get(p.name, (0, 0, 0, 0, 0, 0)): maximum_specificities[p.name] = p.specificity rule["properties"] = properties href = rule["href"] if hasattr(href, "startswith") and href.startswith("file://"): href = href[len("file://") :] if iswindows and href.startswith("/"): href = href[1:] if href: rule["href"] = current_container().abspath_to_name(href, root=self.preview.current_root) @property def current_name(self): return self.preview.current_name @property def is_visible(self): return self.isVisible() def showEvent(self, ev): self.update_timer.start() actions["auto-reload-preview"].setEnabled(True) return QWidget.showEvent(self, ev) def sync_to_editor(self): self.update_data() def update_data(self): if not self.is_visible or self.preview_is_refreshing: return editor_name = self.current_name ed = editors.get(editor_name, None) if self.update_timer.isActive() or (ed is None and editor_name is not None): return QTimer.singleShot(100, self.update_data) if ed is not None: sourceline, tags = ed.current_tag(for_position_sync=False) if self.refresh_needed or self.now_showing != (editor_name, sourceline, tags): self.show_data(editor_name, sourceline, tags) def start_update_timer(self): if self.is_visible: self.update_timer.start() def stop_update_timer(self): self.update_timer.stop() def navigate_to_declaration(self, data, editor): if data["type"] == "inline": sourceline, tags = data["sourceline_address"] editor.goto_sourceline(sourceline, tags, attribute="style") elif data["type"] == "sheet": editor.goto_css_rule(data["rule_address"]) elif data["type"] == "elem": editor.goto_css_rule(data["rule_address"], sourceline_address=data["sourceline_address"])
class ServiceDataQWidget(QWidget): """ Class who create QWidget with service data """ def __init__(self, parent=None): super(ServiceDataQWidget, self).__init__(parent) self.setMinimumSize(460, 230) # Fields self.refresh_timer = QTimer() self.service_item = None self.labels = { 'service_icon': QLabel(), 'service_name': QLabel(), 'ls_last_check': QLabel(), 'ls_output': QLabel(), } self.buttons = { 'acknowledge': QPushButton(), 'downtime': QPushButton() } self.actions_widget = ActionsQWidget() def initialize(self): """ Initialize QWidget """ layout = QGridLayout() self.setLayout(layout) layout.addWidget(self.get_service_icon_widget(), 0, 0, 1, 1) layout.addWidget(self.get_actions_widget(), 0, 1, 1, 1) layout.addWidget(self.get_last_check_widget(), 1, 0, 1, 2) update_service = int(settings.get_config('Alignak-app', 'update_service')) * 1000 self.refresh_timer.setInterval(update_service) self.refresh_timer.start() self.refresh_timer.timeout.connect(self.periodic_refresh) self.hide() def get_service_icon_widget(self): """ Return QWidget with its icon and name :return: widget with icon and name :rtype: QWidget """ widget = QWidget() layout = QVBoxLayout() widget.setLayout(layout) # Host Icon layout.addWidget(self.labels['service_icon']) layout.setAlignment(self.labels['service_icon'], Qt.AlignCenter) # Host Name self.labels['service_name'].setObjectName('itemname') self.labels['service_name'].setWordWrap(True) layout.addWidget(self.labels['service_name']) layout.setAlignment(self.labels['service_name'], Qt.AlignCenter) return widget def get_last_check_widget(self): """ Return QWidget with last check data :return: widget with last check data :rtype: QWidget """ widget = QWidget() layout = QGridLayout() widget.setLayout(layout) # Title check_title = QLabel(_('My last check')) check_title.setObjectName('itemtitle') check_title.setFixedHeight(30) layout.addWidget(check_title, 0, 0, 1, 2) # When last check when_title = QLabel(_("When:")) when_title.setObjectName('title') layout.addWidget(when_title, 2, 0, 1, 1) layout.addWidget(self.labels['ls_last_check'], 2, 1, 1, 1) # Output output_title = QLabel(_("Output")) output_title.setObjectName('title') layout.addWidget(output_title, 3, 0, 1, 1) self.labels['ls_output'].setWordWrap(True) self.labels['ls_output'].setTextInteractionFlags(Qt.TextSelectableByMouse) output_scrollarea = QScrollArea() output_scrollarea.setWidget(self.labels['ls_output']) output_scrollarea.setWidgetResizable(True) output_scrollarea.setObjectName('output') layout.addWidget(output_scrollarea, 3, 1, 1, 1) return widget def get_actions_widget(self): """ Return QWidget with actions buttons :return: widget with buttons :rtype: QWidget """ widget = QWidget() layout = QVBoxLayout() widget.setLayout(layout) action_title = QLabel(_('Actions:')) action_title.setObjectName('title') layout.addWidget(action_title) self.actions_widget.initialize(self.service_item) layout.addWidget(self.actions_widget) layout.setAlignment(Qt.AlignCenter) return widget def update_widget(self, service): """ Update ServiceDataQWidget :param service: Service item with its data :type service: alignak_app.core.models.service.Service """ self.service_item = service monitored = self.service_item.data[ 'passive_checks_enabled'] + self.service_item.data['active_checks_enabled'] icon_name = get_icon_name( 'service', self.service_item.data['ls_state'], self.service_item.data['ls_acknowledged'], self.service_item.data['ls_downtimed'], monitored ) icon_pixmap = QPixmap(settings.get_image(icon_name)) self.labels['service_icon'].setPixmap(QPixmap(icon_pixmap)) self.labels['service_icon'].setScaledContents(True) self.labels['service_icon'].setFixedSize(48, 48) self.labels['service_icon'].setToolTip(self.service_item.get_tooltip()) self.labels['service_name'].setText(self.service_item.get_display_name()) since_last_check = get_diff_since_last_timestamp( self.service_item.data['ls_last_check'] ) last_check_tooltip = get_date_fromtimestamp(self.service_item.data['ls_last_check']) self.labels['ls_last_check'].setText(since_last_check) self.labels['ls_last_check'].setToolTip(last_check_tooltip) self.labels['ls_output'].setText(self.service_item.data['ls_output']) self.actions_widget.item = self.service_item self.actions_widget.update_widget() self.show() def periodic_refresh(self): """ Refresh QWidget periodically """ if self.service_item: updated_service = data_manager.get_item('service', '_id', self.service_item.item_id) if updated_service: self.service_item = updated_service self.update_widget(self.service_item)
class ImageControlDialog(QDialog): def __init__(self, parent, rc, imgman): """An ImageControlDialog is initialized with a parent widget, a RenderControl object, and an ImageManager object""" QDialog.__init__(self, parent) image = rc.image self.setWindowTitle("%s: Colour Controls" % image.name) self.setWindowIcon(pixmaps.colours.icon()) self.setModal(False) self.image = image self._rc = rc self._imgman = imgman self._currier = PersistentCurrier() # init internal state self._prev_range = self._display_range = None, None self._hist = None self._geometry = None # create layouts lo0 = QVBoxLayout(self) # lo0.setContentsMargins(0,0,0,0) # histogram plot whide = self.makeButton("Hide", self.hide, width=128) whide.setShortcut(Qt.Key_F9) lo0.addWidget(Separator(self, "Histogram and ITF", extra_widgets=[whide])) lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) self._histplot = QwtPlot(self) self._histplot.setAutoDelete(False) lo1.addWidget(self._histplot, 1) lo2 = QHBoxLayout() lo2.setContentsMargins(0, 0, 0, 0) lo2.setSpacing(2) lo0.addLayout(lo2) lo0.addLayout(lo1) self._wautozoom = QCheckBox("autozoom", self) self._wautozoom.setChecked(True) self._wautozoom.setToolTip("""<P>If checked, then the histrogram plot will zoom in automatically when you narrow the current intensity range.</P>""") self._wlogy = QCheckBox("log Y", self) self._wlogy.setChecked(True) self._ylogscale = True self._wlogy.setToolTip( """<P>If checked, a log-scale Y axis is used for the histogram plot instead of a linear one.""") self._wlogy.toggled[bool].connect(self._setHistLogScale) self._whistunzoom = self.makeButton("", self._unzoomHistogram, icon=pixmaps.full_range.icon()) self._whistzoomout = self.makeButton("-", self._currier.curry(self._zoomHistogramByFactor, math.sqrt(.1))) self._whistzoomin = self.makeButton("+", self._currier.curry(self._zoomHistogramByFactor, math.sqrt(10))) self._whistzoomin.setToolTip("""<P>Click to zoom into the histogram plot by one step. This does not change the current intensity range.</P>""") self._whistzoomout.setToolTip("""<P>Click to zoom out of the histogram plot by one step. This does not change the current intensity range.</P>""") self._whistunzoom.setToolTip("""<P>Click to reset the histogram plot back to its full extent. This does not change the current intensity range.</P>""") self._whistzoom = QwtWheel(self) self._whistzoom.setMass(0.5) self._whistzoom.setOrientation(Qt.Horizontal) self._whistzoom.setMaximumWidth(80) self._whistzoom.setRange(0, 10) self._whistzoom.setSingleStep(0.1) self._whistzoom.setPageStepCount(1) self._whistzoom.setTickCount(30) self._whistzoom.setTracking(False) self._whistzoom.valueChanged['double'].connect(self._zoomHistogramFinalize) self._whistzoom.wheelMoved['double'].connect(self._zoomHistogramPreview) self._whistzoom.setToolTip("""<P>Use this wheel control to zoom in/out of the histogram plot. This does not change the current intensity range. Note that the zoom wheel should also respond to your mouse wheel, if you have one.</P>""") # This works around a stupid bug in QwtSliders -- when using the mousewheel, only sliderMoved() signals are emitted, # with no final valueChanged(). If we want to do a fast preview of something on sliderMoved(), and a "slow" final # step on valueChanged(), we're in trouble. So we start a timer on sliderMoved(), and if the timer expires without # anything else happening, do a valueChanged(). # Here we use a timer to call zoomHistogramFinalize() w/o an argument. self._whistzoom_timer = QTimer(self) self._whistzoom_timer.setSingleShot(True) self._whistzoom_timer.setInterval(500) self._whistzoom_timer.timeout.connect(self._zoomHistogramFinalize) # set same size for all buttons and controls width = 24 for w in self._whistunzoom, self._whistzoomin, self._whistzoomout: w.setMinimumSize(width, width) w.setMaximumSize(width, width) self._whistzoom.setMinimumSize(80, width) self._wlab_histpos_text = "(hover for help)" self._wlab_histpos = QLabel(self._wlab_histpos_text, self) help_font = QFont() help_font.setPointSize(8) self._wlab_histpos.setFont(help_font) self._wlab_histpos.setToolTip(""" <P>The plot shows a histogram of either the full image or its selected subset (as per the "Data subset" section below).</P> <P>The current intensity range is indicated by the grey box in the plot.</P> <P>Use the left mouse button to change the low intensity limit, and the right button (on Macs, use Ctrl-click) to change the high limit.</P> <P>Use Shift with the left mouse button to zoom into an area of the histogram, or else use the "zoom wheel" control or the plus/minus toolbuttons above the histogram to zoom in or out. To zoom back out to the full extent of the histogram, click on the rightmost button above the histogram.</P> """) lo2.addWidget(self._wlab_histpos, 1) lo2.addWidget(self._wautozoom) lo2.addWidget(self._wlogy, 0) lo2.addWidget(self._whistzoomin, 0) lo2.addWidget(self._whistzoom, 0) lo2.addWidget(self._whistzoomout, 0) lo2.addWidget(self._whistunzoom, 0) self._zooming_histogram = False sliced_axes = rc.slicedAxes() dprint(1, "sliced axes are", sliced_axes) self._stokes_axis = None # subset indication lo0.addWidget(Separator(self, "Data subset")) # sliced axis selectors self._wslicers = [] if sliced_axes: lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo1.setSpacing(2) lo0.addLayout(lo1) lo1.addWidget(QLabel("Current slice: ", self)) for i, (iextra, name, labels) in enumerate(sliced_axes): lo1.addWidget(QLabel("%s:" % name, self)) if name == "STOKES": self._stokes_axis = iextra # add controls wslicer = QComboBox(self) self._wslicers.append(wslicer) wslicer.addItems(labels) wslicer.setToolTip("""<P>Selects current slice along the %s axis.</P>""" % name) wslicer.setCurrentIndex(self._rc.currentSlice()[iextra]) wslicer.activated[int].connect(self._currier.curry(self._rc.changeSlice, iextra)) lo2 = QVBoxLayout() lo1.addLayout(lo2) lo2.setContentsMargins(0, 0, 0, 0) lo2.setSpacing(0) wminus = QToolButton(self) wminus.setArrowType(Qt.UpArrow) wminus.clicked.connect(self._currier.curry(self._rc.incrementSlice, iextra, 1)) if i == 0: wminus.setShortcut(Qt.SHIFT + Qt.Key_F7) elif i == 1: wminus.setShortcut(Qt.SHIFT + Qt.Key_F8) wplus = QToolButton(self) wplus.setArrowType(Qt.DownArrow) wplus.clicked.connect(self._currier.curry(self._rc.incrementSlice, iextra, -1)) if i == 0: wplus.setShortcut(Qt.Key_F7) elif i == 1: wplus.setShortcut(Qt.Key_F8) wminus.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) wplus.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) sz = QSize(12, 8) wminus.setMinimumSize(sz) wplus.setMinimumSize(sz) wminus.resize(sz) wplus.resize(sz) lo2.addWidget(wminus) lo2.addWidget(wplus) lo1.addWidget(wslicer) lo1.addSpacing(5) lo1.addStretch(1) # subset indicator lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo1.setSpacing(2) lo0.addLayout(lo1) self._wlab_subset = QLabel("Subset: xxx", self) self._wlab_subset.setToolTip("""<P>This indicates the current data subset to which the histogram and the stats given here apply. Use the "Reset to" control on the right to change the current subset and recompute the histogram and stats.</P>""") lo1.addWidget(self._wlab_subset, 1) self._wreset_full = self.makeButton("\u2192 full", self._rc.setFullSubset) lo1.addWidget(self._wreset_full) if sliced_axes: # if self._stokes_axis is not None and len(sliced_axes)>1: # self._wreset_stokes = self.makeButton(u"\u21920Stokes",self._rc.setFullSubset) self._wreset_slice = self.makeButton("\u2192 slice", self._rc.setSliceSubset) lo1.addWidget(self._wreset_slice) else: self._wreset_slice = None # min/max controls lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo0.addLayout(lo1, 0) self._wlab_stats = QLabel(self) self._wlab_stats.setWordWrap(True) self._wlab_stats.setMinimumWidth(384) lo1.addWidget(self._wlab_stats, 0) self._wmore_stats = self.makeButton("more...", self._showMeanStd) self._wlab_stats.setMinimumHeight(self._wmore_stats.height()) lo1.addWidget(self._wmore_stats, 0) lo1.addStretch(1) # intensity controls lo0.addWidget(Separator(self, "Intensity mapping")) lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo1.setSpacing(2) lo0.addLayout(lo1, 0) self._range_validator = FloatValidator(self) self._wrange = QLineEdit(self), QLineEdit(self) self._wrange[0].setToolTip("""<P>This is the low end of the intensity range.</P>""") self._wrange[1].setToolTip("""<P>This is the high end of the intensity range.</P>""") for w in self._wrange: w.setValidator(self._range_validator) w.editingFinished.connect(self._changeDisplayRange) lo1.addWidget(QLabel("low:", self), 0) lo1.addWidget(self._wrange[0], 1) self._wrangeleft0 = self.makeButton("\u21920", self._setZeroLeftLimit, width=32) self._wrangeleft0.setToolTip("""<P>Click this to set the low end of the intensity range to 0.</P>""") lo1.addWidget(self._wrangeleft0, 0) lo1.addSpacing(8) lo1.addWidget(QLabel("high:", self), 0) lo1.addWidget(self._wrange[1], 1) lo1.addSpacing(8) self._wrange_full = self.makeButton(None, self._setHistDisplayRange, icon=pixmaps.intensity_graph.icon()) lo1.addWidget(self._wrange_full) self._wrange_full.setToolTip( """<P>Click this to reset the intensity range to the current extent of the histogram plot.</P>""") # add menu for display range range_menu = QMenu(self) wrange_menu = QToolButton(self) wrange_menu.setText("Reset to") wrange_menu.setToolTip("""<P>Use this to reset the intensity range to various pre-defined settings.</P>""") lo1.addWidget(wrange_menu) self._qa_range_full = range_menu.addAction(pixmaps.full_range.icon(), "Full subset", self._rc.resetSubsetDisplayRange) self._qa_range_hist = range_menu.addAction(pixmaps.intensity_graph.icon(), "Current histogram limits", self._setHistDisplayRange) for percent in (99.99, 99.9, 99.5, 99, 98, 95): range_menu.addAction("%g%%" % percent, self._currier.curry(self._changeDisplayRangeToPercent, percent)) wrange_menu.setMenu(range_menu) wrange_menu.setPopupMode(QToolButton.InstantPopup) lo1 = QGridLayout() lo1.setContentsMargins(0, 0, 0, 0) lo0.addLayout(lo1, 0) self._wimap = QComboBox(self) lo1.addWidget(QLabel("Intensity policy:", self), 0, 0) lo1.addWidget(self._wimap, 1, 0) self._wimap.addItems(rc.getIntensityMapNames()) self._wimap.currentIndexChanged[int].connect(self._rc.setIntensityMapNumber) self._wimap.setToolTip("""<P>Use this to change the type of the intensity transfer function (ITF).</P>""") # log cycles control lo1.setColumnStretch(1, 1) self._wlogcycles_label = QLabel("Log cycles: ", self) lo1.addWidget(self._wlogcycles_label, 0, 1) # self._wlogcycles = QwtWheel(self) # self._wlogcycles.setTotalAngle(360) self._wlogcycles = QwtSlider(self) self._wlogcycles.setToolTip( """<P>Use this to change the log-base for the logarithmic intensity transfer function (ITF).</P>""") # This works around a stupid bug in QwtSliders -- see comments on histogram zoom wheel above self._wlogcycles_timer = QTimer(self) self._wlogcycles_timer.setSingleShot(True) self._wlogcycles_timer.setInterval(500) self._wlogcycles_timer.timeout.connect(self._setIntensityLogCycles) lo1.addWidget(self._wlogcycles, 1, 1) # self._wlogcycles.setRange(1., 10) # need to find 6.1.5 change from v5 self._wlogcycles.setScale(1., 10) # self._wlogcycles.setStep(0.1) # need to find 6.1.5 change from v5 # self._wlogcycles.setScaleStepSize(0.1) self._wlogcycles.setTracking(False) self._wlogcycles.valueChanged.connect(self._setIntensityLogCycles) self._wlogcycles.sliderMoved.connect(self._previewIntensityLogCycles) self._updating_imap = False # lock intensity map lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo0.addLayout(lo1, 0) # lo1.addWidget(QLabel("Lock range accross",self)) wlock = QCheckBox("Lock display range", self) wlock.setMinimumWidth(192) wlock.setToolTip("""<P>If checked, then the intensity range will be locked. The ranges of all locked images change simultaneously.</P>""") lo1.addWidget(wlock) wlockall = QToolButton(self) wlockall.setIcon(pixmaps.locked.icon()) wlockall.setText("Lock all to this") wlockall.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) wlockall.setAutoRaise(True) wlockall.setToolTip("""<P>Click this to lock together the intensity ranges of all images.</P>""") lo1.addWidget(wlockall) wunlockall = QToolButton(self) wunlockall.setIcon(pixmaps.unlocked.icon()) wunlockall.setText("Unlock all") wunlockall.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) wunlockall.setAutoRaise(True) wunlockall.setToolTip("""<P>Click this to unlock the intensity ranges of all images.</P>""") lo1.addWidget(wunlockall) wlock.setChecked(self._rc.isDisplayRangeLocked()) wlock.clicked[bool].connect(self._rc.lockDisplayRange) wlockall.clicked.connect(self._currier.curry(self._imgman.lockAllDisplayRanges, self._rc)) wunlockall.clicked.connect(self._imgman.unlockAllDisplayRanges) self._rc.displayRangeLocked.connect(wlock.setChecked) # self._wlock_imap_axis = [ QCheckBox(name,self) for iaxis,name,labels in sliced_axes ] # for iw,w in enumerate(self._wlock_imap_axis): # QObject.connect(w,pyqtSignal("toggled(bool)"),self._currier.curry(self._rc.lockDisplayRangeForAxis,iw)) # lo1.addWidget(w,0) lo1.addStretch(1) # lo0.addWidget(Separator(self,"Colourmap")) # color bar self._colorbar = QwtPlot(self) lo0.addWidget(self._colorbar) self._colorbar.setAutoDelete(False) self._colorbar.setMinimumHeight(32) self._colorbar.enableAxis(QwtPlot.yLeft, False) self._colorbar.enableAxis(QwtPlot.xBottom, False) # color plot self._colorplot = QwtPlot(self) lo0.addWidget(self._colorplot) self._colorplot.setAutoDelete(False) self._colorplot.setMinimumHeight(64) self._colorplot.enableAxis(QwtPlot.yLeft, False) self._colorplot.enableAxis(QwtPlot.xBottom, False) # self._colorplot.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Preferred) self._colorbar.hide() self._colorplot.hide() # color controls lo1 = QHBoxLayout() lo1.setContentsMargins(0, 0, 0, 0) lo0.addLayout(lo1, 1) lo1.addWidget(QLabel("Colourmap:", self)) # colormap list ### NB: use setIconSize() and icons in QComboBox!!! self._wcolmaps = QComboBox(self) self._wcolmaps.setIconSize(QSize(128, 16)) self._wcolmaps.setToolTip("""<P>Use this to select a different colourmap.</P>""") for cmap in self._rc.getColormapList(): self._wcolmaps.addItem(QIcon(cmap.makeQPixmap(128, 16)), cmap.name) lo1.addWidget(self._wcolmaps) self._wcolmaps.activated[int].connect(self._rc.setColorMapNumber) # add widgetstack for colormap controls self._wcolmap_control_stack = QStackedWidget(self) self._wcolmap_control_blank = QWidget(self._wcolmap_control_stack) self._wcolmap_control_stack.addWidget(self._wcolmap_control_blank) lo0.addWidget(self._wcolmap_control_stack) self._colmap_controls = [] # add controls to stack for index, cmap in enumerate(self._rc.getColormapList()): if isinstance(cmap, Colormaps.ColormapWithControls): controls = cmap.makeControlWidgets(self._wcolmap_control_stack) self._wcolmap_control_stack.addWidget(controls) cmap.colormapChanged.connect(self._currier.curry(self._previewColormapParameters, index, cmap)) cmap.colormapPreviewed.connect(self._currier.curry(self._previewColormapParameters, index, cmap)) self._colmap_controls.append(controls) else: self._colmap_controls.append(self._wcolmap_control_blank) # connect updates from renderControl and image self.image.signalSlice.connect(self._updateImageSlice) self._rc.intensityMapChanged.connect(self._updateIntensityMap) self._rc.colorMapChanged.connect(self._updateColorMap) self._rc.dataSubsetChanged.connect(self._updateDataSubset) self._rc.displayRangeChanged.connect(self._updateDisplayRange) # update widgets self._setupHistogramPlot() self._updateDataSubset(*self._rc.currentSubset()) self._updateColorMap(image.colorMap()) self._updateIntensityMap(rc.currentIntensityMap(), rc.currentIntensityMapNumber()) self._updateDisplayRange(*self._rc.displayRange()) def makeButton(self, label, callback=None, width=None, icon=None): btn = QToolButton(self) # btn.setAutoRaise(True) label and btn.setText(label) icon and btn.setIcon(icon) # btn = QPushButton(label,self) # btn.setFlat(True) if width: btn.setMinimumWidth(width) btn.setMaximumWidth(width) if icon: btn.setIcon(icon) if callback: btn.clicked.connect(callback) return btn # def closeEvent (self,ev): # ev.ignore() # self.hide() def hide(self): self._geometry = self.geometry() QDialog.hide(self) self.parent().setVisible(False) def show(self): dprint(4, "show entrypoint") if self._geometry: dprint(4, "setting geometry") self.setGeometry(self._geometry) if self._hist is None: busy = BusyIndicator() dprint(4, "updating histogram") self._updateHistogram() dprint(4, "updating stats") self._updateStats(self._subset, self._subset_range) busy.reset_cursor() dprint(4, "calling QDialog.show") QDialog.show(self) # number of bins used to compute intensity transfer function NumItfBins = 1000 # number of bins used for displaying histograms NumHistBins = 500 # number of bins used for high-res histograms NumHistBinsHi = 10000 # colorbar height, as fraction of plot area ColorBarHeight = 0.1 class HistLimitPicker(QwtPlotPicker): """Auguments QwtPlotPicker with functions for selecting hist min/max values""" def __init__(self, plot, label, color="green", mode=QwtPickerClickPointMachine(), rubber_band=QwtPicker.VLineRubberBand, tracker_mode=QwtPicker.ActiveOnly, track=None): QwtPlotPicker.__init__(self, QwtPlot.xBottom, QwtPlot.yRight, rubber_band, tracker_mode, plot.canvas()) self.setStateMachine(mode) self.plot = plot self.label = label self.track = track self.color = QColor(color) self.setRubberBandPen(QPen(self.color)) self.setRubberBand(rubber_band) def trackerText(self, pos): x, y = self.plot.invTransform(QwtPlot.xBottom, pos.x()), self.plot.invTransform(QwtPlot.yLeft, pos.y()) if self.track: text = self.track(x, y) if text is not None: return text if self.label: text = QwtText(self.label % dict(x=x, y=y)) text.setColor(self.color) return text return QwtText() def widgetLeaveEvent(self, ev): if self.track: self.track(None, None) QwtPlotPicker.widgetLeaveEvent(self, ev) class ColorBarPlotItem(QwtPlotItem): def __init__(self, y0, y1, *args): QwtPlotItem.__init__(self, *args) self.RenderAntialiased self.imap = None self.cmap = None self._y0 = y1 self._dy = y1 - y0 def setIntensityMap(self, imap): self.imap = imap def setColorMap(self, cmap): self.cmap = cmap def draw(self, painter, xmap, ymap, rect): """Implements QwtPlotItem.draw(), to render the colorbar on the given painter.""" xp1, xp2, xdp, xs1, xs2, xds = xinfo = xmap.p1(), xmap.p2(), xmap.pDist(), xmap.s1(), xmap.s2(), xmap.sDist() yp1, yp2, ydp, ys1, ys2, yds = yinfo = ymap.p1(), ymap.p2(), ymap.pDist(), ymap.s1(), ymap.s2(), ymap.sDist() # xp: coordinates of pixels xp1...xp2 in data units xp = xs1 + (xds / xdp) * (0.5 + numpy.arange(int(xdp))) # convert y0 and y1 into pixel coordinates y0 = yp1 - (self._y0 - ys1) * (ydp / yds) dy = self._dy * (ydp / yds) # remap into an Nx1 image qimg = self.cmap.colorize(self.imap.remap(xp.reshape((len(xp), 1)))) # plot image painter.drawImage(QRect(xp1, y0, xdp, dy), qimg) class HistogramLineMarker: """Helper class implementing a line marker for a histogram plot""" def __init__(self, plot, color="black", linestyle=Qt.DotLine, align=Qt.AlignBottom | Qt.AlignRight, z=90, label="", zlabel=None, linewidth=1, spacing=2, yaxis=QwtPlot.yRight): self.line = TiggerPlotCurve() self.line.setRenderHint(QwtPlotItem.RenderAntialiased) self.color = color = color if isinstance(color, QColor) else QColor(color) self.line.setPen(QPen(color, linewidth, linestyle)) self.marker = TiggerPlotMarker() self.marker.setRenderHint(QwtPlotItem.RenderAntialiased) self.marker.setLabelAlignment(align) try: self.marker.setSpacing(spacing) except AttributeError: pass self.setText(label) self.line.setZ(z) self.marker.setZ(zlabel if zlabel is not None else z) # set axes -- using yRight, since that is the "markup" z-axis self.line.setAxes(QwtPlot.xBottom, yaxis) self.marker.setAxes(QwtPlot.xBottom, yaxis) # attach to plot self.line.attach(plot) self.marker.attach(plot) def show(self): self.line.show() self.marker.show() def hide(self): self.line.hide() self.marker.hide() def setText(self, text): label = QwtText(text) label.setColor(self.color) self.marker.setLabel(label) def _setupHistogramPlot(self): self._histplot.setCanvasBackground(QColor("lightgray")) self._histplot.setAxisFont(QwtPlot.yLeft, QApplication.font()) self._histplot.setAxisFont(QwtPlot.xBottom, QApplication.font()) # add histogram curves self._histcurve1 = TiggerPlotCurve() self._histcurve1.setRenderHint(QwtPlotItem.RenderAntialiased) self._histcurve2 = TiggerPlotCurve() self._histcurve2.setRenderHint(QwtPlotItem.RenderAntialiased) self._histcurve1.setStyle(QwtPlotCurve.Steps) self._histcurve2.setStyle(QwtPlotCurve.Steps) self._histcurve1.setPen(QPen(Qt.NoPen)) self._histcurve1.setBrush(QBrush(QColor("slategrey"))) pen = QPen(QColor("red")) pen.setWidth(1) self._histcurve2.setPen(pen) self._histcurve1.setZ(0) self._histcurve2.setZ(100) # self._histcurve1.attach(self._histplot) self._histcurve2.attach(self._histplot) # add maxbin and half-max curves self._line_0 = self.HistogramLineMarker(self._histplot, color="grey50", linestyle=Qt.SolidLine, align=Qt.AlignTop | Qt.AlignLeft, z=90) self._line_mean = self.HistogramLineMarker(self._histplot, color="black", linestyle=Qt.SolidLine, align=Qt.AlignBottom | Qt.AlignRight, z=91, label="mean", zlabel=151) self._line_std = self.HistogramLineMarker(self._histplot, color="black", linestyle=Qt.SolidLine, align=Qt.AlignTop | Qt.AlignRight, z=91, label="std", zlabel=151) sym = QwtSymbol() sym.setStyle(QwtSymbol.VLine) sym.setSize(8) self._line_std.line.setSymbol(sym) self._line_maxbin = self.HistogramLineMarker(self._histplot, color="green", linestyle=Qt.DotLine, align=Qt.AlignTop | Qt.AlignRight, z=92, label="max bin", zlabel=150) self._line_halfmax = self.HistogramLineMarker(self._histplot, color="green", linestyle=Qt.DotLine, align=Qt.AlignBottom | Qt.AlignRight, z=90, label="half-max", yaxis=QwtPlot.yLeft) # add current range self._rangebox = TiggerPlotCurve() self._rangebox.setRenderHint(QwtPlotItem.RenderAntialiased) self._rangebox.setStyle(QwtPlotCurve.Steps) self._rangebox.setYAxis(QwtPlot.yRight) self._rangebox.setPen(QPen(Qt.NoPen)) self._rangebox.setBrush(QBrush(QColor("darkgray"))) self._rangebox.setZ(50) self._rangebox.attach(self._histplot) self._rangebox2 = TiggerPlotCurve() self._rangebox2.setRenderHint(QwtPlotItem.RenderAntialiased) self._rangebox2.setStyle(QwtPlotCurve.Sticks) self._rangebox2.setYAxis(QwtPlot.yRight) self._rangebox2.setZ(60) # self._rangebox2.attach(self._histplot) # add intensity transfer function self._itfcurve = TiggerPlotCurve() self._itfcurve.setRenderHint(QwtPlotItem.RenderAntialiased) self._itfcurve.setStyle(QwtPlotCurve.Lines) self._itfcurve.setPen(QPen(QColor("blue"))) self._itfcurve.setYAxis(QwtPlot.yRight) self._itfcurve.setZ(120) self._itfcurve.attach(self._histplot) self._itfmarker = TiggerPlotMarker() self._itfmarker.setRenderHint(QwtPlotItem.RenderAntialiased) label = QwtText("ITF") label.setColor(QColor("blue")) self._itfmarker.setLabel(label) try: self._itfmarker.setSpacing(0) except AttributeError: pass self._itfmarker.setLabelAlignment(Qt.AlignTop | Qt.AlignRight) self._itfmarker.setZ(120) self._itfmarker.attach(self._histplot) # add colorbar self._cb_item = self.ColorBarPlotItem(1, 1 + self.ColorBarHeight) self._cb_item.setYAxis(QwtPlot.yRight) self._cb_item.attach(self._histplot) # add pickers self._hist_minpicker = self.HistLimitPicker(self._histplot, "low: %(x).4g") self._hist_minpicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton) self._hist_minpicker.selected.connect(self._selectLowLimit) self._hist_maxpicker = self.HistLimitPicker(self._histplot, "high: %(x).4g") self._hist_maxpicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.RightButton) self._hist_maxpicker.selected.connect(self._selectHighLimit) self._hist_maxpicker1 = self.HistLimitPicker(self._histplot, "high: %(x).4g") self._hist_maxpicker1.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton, Qt.ControlModifier) self._hist_maxpicker1.selected.connect(self._selectHighLimit) self._hist_zoompicker = self.HistLimitPicker(self._histplot, label="zoom", tracker_mode=QwtPicker.AlwaysOn, track=self._trackHistCoordinates, color="black", mode=QwtPickerClickRectMachine(), rubber_band=QwtPicker.RectRubberBand) self._hist_zoompicker.setMousePattern(QwtEventPattern.MouseSelect1, Qt.LeftButton, Qt.ShiftModifier) # self._hist_zoompicker.selected[QRectF].connect(self._zoomHistogramIntoRect) self._hist_zoompicker.selected.connect(self._zoomHistogramIntoRect) def _trackHistCoordinates(self, x, y): self._wlab_histpos.setText((DataValueFormat + " %d") % (x, y) if x is not None else self._wlab_histpos_text) return QwtText() def _updateITF(self): """Updates current ITF array.""" # do nothing if no histogram -- means we're not visible if self._hist is not None: xdata = self._itf_bins ydata = self.image.intensityMap().remap(xdata) self._rangebox.setData(self._rc.displayRange(), [1, 1]) self._rangebox2.setData(self._rc.displayRange(), [1, 1]) self._itfcurve.setData(xdata, ydata) self._itfmarker.setValue(xdata[0], 1) def _updateHistogram(self, hmin=None, hmax=None): """Recomputes histogram. If no arguments, computes full histogram for data subset. If hmin/hmax is specified, computes zoomed-in histogram.""" busy = BusyIndicator() self._prev_range = self._display_range dmin, dmax = self._subset_range hmin0, hmax0 = dmin, dmax if hmin0 >= hmax0: hmax0 = hmin0 + 1 subset, mask = self.image.optimalRavel(self._subset) # compute full-subset hi-res histogram, if we don't have one (for percentile stats) if self._hist_hires is None: dprint(1, "computing histogram for full subset range", hmin0, hmax0) self._hist_hires = measurements.histogram(subset, hmin0, hmax0, self.NumHistBinsHi, labels=mask, index=None if mask is None else False) self._hist_bins_hires = hmin0 + (hmax0 - hmin0) * (numpy.arange(self.NumHistBinsHi) + 0.5) / float( self.NumHistBinsHi) self._hist_binsize_hires = (hmax0 - hmin0) / self.NumHistBins # if hist limits not specified, then compute lo-res histogram based on the hi-res one if hmin is None: hmin, hmax = hmin0, hmax0 # downsample to low-res histogram self._hist = self._hist_hires.reshape((self.NumHistBins, int(self.NumHistBinsHi / self.NumHistBins))).sum(1) else: # zoomed-in low-res histogram # bracket limits at subset range hmin, hmax = max(hmin, dmin), min(hmax, dmax) if hmin >= hmax: hmax = hmin + 1 dprint(1, "computing histogram for", self._subset.shape, self._subset.dtype, hmin, hmax) self._hist = measurements.histogram(subset, hmin, hmax, self.NumHistBins, labels=mask, index=None if mask is None else False) dprint(1, "histogram computed") # compute bins self._itf_bins = hmin + (hmax - hmin) * (numpy.arange(self.NumItfBins)) / (float(self.NumItfBins) - 1) self._hist_bins = hmin + (hmax - hmin) * (numpy.arange(self.NumHistBins) + 0.5) / float(self.NumHistBins) # histogram range and position of peak self._hist_range = hmin, hmax self._hist_min, self._hist_max, self._hist_imin, self._hist_imax = measurements.extrema(self._hist) self._hist_peak = self._hist_bins[self._hist_imax] # set controls accordingly if dmin >= dmax: dmax = dmin + 1 zoom = math.log10((dmax - dmin) / (hmax - hmin)) self._whistzoom.setValue(zoom) self._whistunzoom.setEnabled(zoom > 0) self._whistzoomout.setEnabled(zoom > 0) # reset scales self._histplot.setAxisScale(QwtPlot.xBottom, hmin, hmax) self._histplot.setAxisScale(QwtPlot.yRight, 0, 1 + self.ColorBarHeight) # update curves # call _setHistLogScale() (with current setting) to update axis scales and set data self._setHistLogScale(self._ylogscale, replot=False) # set plot lines self._line_0.line.setData([0, 0], [0, 1]) self._line_0.marker.setValue(0, 0) self._line_maxbin.line.setData([self._hist_peak, self._hist_peak], [0, 1]) self._line_maxbin.marker.setValue(self._hist_peak, 0) self._line_maxbin.setText(("max bin:" + DataValueFormat) % self._hist_peak) # set half-max line self._line_halfmax.line.setData(self._hist_range, [self._hist_max / 2, self._hist_max / 2]) self._line_halfmax.marker.setValue(hmin, self._hist_max / 2) # update ITF self._updateITF() busy.reset_cursor() def _updateStats(self, subset, minmax): """Recomputes subset statistics.""" if subset.size <= (2048 * 2048): self._showMeanStd(busy=False) else: self._wlab_stats.setText( ("min: %s max: %s np: %d" % (DataValueFormat, DataValueFormat, self._subset.size)) % minmax) self._wmore_stats.show() def _updateDataSubset(self, subset, minmax, desc, subset_type): """Called when the displayed data subset is changed. Updates the histogram.""" self._subset = subset self._subset_range = minmax self._wlab_subset.setText("Subset: %s" % desc) self._hist = self._hist_hires = None self._wreset_full.setVisible(subset_type is not RenderControl.SUBSET_FULL) self._wreset_slice and self._wreset_slice.setVisible(subset_type is not RenderControl.SUBSET_SLICE) # hide the mean/std markers, they will only be shown when _showMeanStd() is called self._line_mean.hide() self._line_std.hide() # if we're visibile, recompute histograms and stats if self.isVisible(): # if subset is sufficiently small, compute extended stats on-the-fly. Else show the "more" button to compute them later self._updateHistogram() self._updateStats(subset, minmax) self._histplot.replot() def _showMeanStd(self, busy=True): if busy: busy = BusyIndicator() dmin, dmax = self._subset_range subset, mask = self.image.optimalRavel(self._subset) dprint(5, "computing mean") mean = measurements.mean(subset, labels=mask, index=None if mask is None else False) dprint(5, "computing std") std = measurements.standard_deviation(subset, labels=mask, index=None if mask is None else False) dprint(5, "done") text = " ".join([("%s: " + DataValueFormat) % (name, value) for name, value in (("min", dmin), ("max", dmax), ("mean", mean), ("\n std", std))] + ["np: %d" % self._subset.size]) self._wlab_stats.setText(text) self._wmore_stats.hide() # update markers ypos = 0.3 self._line_mean.line.setData([mean, mean], [0, 1]) self._line_mean.marker.setValue(mean, ypos) self._line_mean.setText(("\u03BC=" + DataValueFormat) % mean) self._line_mean.show() self._line_std.line.setData([mean - std, mean + std], [ypos, ypos]) self._line_std.marker.setValue(mean, ypos) self._line_std.setText(("\u03C3=" + DataValueFormat) % std) self._line_std.show() self._histplot.replot() if not isinstance(busy, bool): busy.reset_cursor() def _setIntensityLogCyclesLabel(self, value): self._wlogcycles_label.setText("Log cycles: %4.1f" % value) def _previewIntensityLogCycles(self, value): self._setIntensityLogCycles(value, notify_image=False, write_config=False) self._wlogcycles_timer.start(500) def _setIntensityLogCycles(self, value=None, notify_image=True, write_config=True): if value is None: value = self._wlogcycles.value() # stop timer if being called to finalize the change in value if notify_image: self._wlogcycles_timer.stop() if not self._updating_imap: self._setIntensityLogCyclesLabel(value) self._rc.setIntensityMapLogCycles(value, notify_image=notify_image, write_config=write_config) self._updateITF() self._histplot.replot() def _updateDisplayRange(self, dmin, dmax): self._rangebox.setData([dmin, dmax], [.9, .9]) self._wrange[0].setText(DataValueFormat % dmin) self._wrange[1].setText(DataValueFormat % dmax) self._wrangeleft0.setEnabled(dmin != 0) self._display_range = dmin, dmax # if auto-zoom is on, zoom the histogram # try to be a little clever about this. Zoom only if (a) both limits have changed (so that adjusting one end of the range # does not cause endless rezooms), or (b) display range is < 1/10 of the histogram range if self._wautozoom.isChecked() and self._hist is not None: if (dmax - dmin) / (self._hist_range[1] - self._hist_range[0]) < .1 or ( dmin != self._prev_range[0] and dmax != self._prev_range[1]): margin = (dmax - dmin) / 8 self._updateHistogram(dmin - margin, dmax + margin) self._updateITF() self._histplot.replot() def _updateIntensityMap(self, imap, index): self._updating_imap = True try: self._cb_item.setIntensityMap(imap) self._updateITF() self._histplot.replot() self._wimap.setCurrentIndex(index) if isinstance(imap, Colormaps.LogIntensityMap): self._wlogcycles.setValue(imap.log_cycles) self._setIntensityLogCyclesLabel(imap.log_cycles) self._wlogcycles.show() self._wlogcycles_label.show() else: self._wlogcycles.hide() self._wlogcycles_label.hide() finally: self._updating_imap = False def _updateColorMap(self, cmap): self._cb_item.setColorMap(cmap) self._histplot.replot() try: index = self._rc.getColormapList().index(cmap) except: return self._setCurrentColormapNumber(index, cmap) def _previewColormapParameters(self, index, cmap): """Called to preview a new colormap parameter value""" self._histplot.replot() self._wcolmaps.setItemIcon(index, QIcon(cmap.makeQPixmap(128, 16))) def _setCurrentColormapNumber(self, index, cmap): self._wcolmaps.setCurrentIndex(index) # show controls for colormap self._wcolmap_control_stack.setCurrentWidget(self._colmap_controls[index]) def _changeDisplayRange(self): """Gets display range from widgets and updates the image with it.""" try: newrange = [float(str(w.text())) for w in self._wrange] except ValueError: return self._rc.setDisplayRange(*newrange) def _setHistDisplayRange(self): self._rc.setDisplayRange(*self._hist_range) def _updateImageSlice(self, _slice): for i, (iextra, name, labels) in enumerate(self._rc.slicedAxes()): self._wslicers[i].setCurrentIndex(_slice[iextra]) def _changeDisplayRangeToPercent(self, percent): busy = BusyIndicator() if self._hist is None: self._updateHistogram() self._updateStats(self._subset, self._subset_range) # delta: we need the [delta,100-delta] interval of the total distribution delta = self._subset.size * ((100. - percent) / 200.) # get F(x): cumulative sum cumsum = numpy.zeros(len(self._hist_hires) + 1, dtype=int) cumsum[1:] = numpy.cumsum(self._hist_hires) bins = numpy.zeros(len(self._hist_hires) + 1, dtype=float) bins[0] = self._subset_range[0] bins[1:] = self._hist_bins_hires + self._hist_binsize_hires / 2 # use interpolation to find value interval corresponding to [delta,100-delta] of the distribution dprint(2, self._subset.size, delta, self._subset.size - delta) dprint(2, cumsum, self._hist_bins_hires) # if first bin is already > delta, then set colour range to first bin x0, x1 = numpy.interp([delta, self._subset.size - delta], cumsum, bins) # and change the display range (this will also cause a histplot.replot() via _updateDisplayRange above) self._rc.setDisplayRange(x0, x1) busy.reset_cursor() def _setZeroLeftLimit(self): self._rc.setDisplayRange(0., self._rc.displayRange()[1]) def _selectLowLimit(self, pos): self._rc.setDisplayRange(pos.x(), self._rc.displayRange()[1]) def _selectHighLimit(self, pos): self._rc.setDisplayRange(self._rc.displayRange()[0], pos.x()) def _unzoomHistogram(self): self._updateHistogram() self._histplot.replot() def _zoomHistogramByFactor(self, factor, curry=None): """ Changes histogram limits by specified factor. curry=None is due to an error raised from the signal to zoom and is unused. """ # get max distance of plot limit from peak dprint(1, "zooming histogram by", factor) halfdist = (self._hist_range[1] - self._hist_range[0]) / (factor * 2) self._updateHistogram(self._hist_peak - halfdist, self._hist_peak + halfdist) self._histplot.replot() def _zoomHistogramIntoRect(self, rect): hmin, hmax = rect.bottomLeft().x(), rect.bottomRight().x() if hmax > hmin: self._updateHistogram(rect.bottomLeft().x(), rect.bottomRight().x()) self._histplot.replot() def _zoomHistogramPreview(self, value): dprint(2, "wheel moved to", value) self._zoomHistogramFinalize(value, preview=True) self._whistzoom_timer.start() def _zoomHistogramFinalize(self, value=None, preview=False): if self._zooming_histogram: return self._zooming_histogram = True try: if value is not None: dmin, dmax = self._subset_range dist = max(dmax - self._hist_peak, self._hist_peak - dmin) / 10 ** value self._preview_hist_range = max(self._hist_peak - dist, dmin), min(self._hist_peak + dist, dmax) if preview: self._histplot.setAxisScale(QwtPlot.xBottom, *self._preview_hist_range) else: dprint(2, "wheel finalized at", value) self._whistzoom_timer.stop() self._updateHistogram(*self._preview_hist_range) self._histplot.replot() finally: self._zooming_histogram = False def _setHistLogScale(self, logscale, replot=True): self._ylogscale = logscale if logscale: self._histplot.setAxisScaleEngine(QwtPlot.yLeft, QwtLogScaleEngine()) ymax = max(1, self._hist_max) self._histplot.setAxisScale(QwtPlot.yLeft, 1, 10 ** (math.log10(ymax) * (1 + self.ColorBarHeight))) y = self._hist.copy() y[y == 0] = 1 self._histcurve1.setData(self._hist_bins, y) self._histcurve2.setData(self._hist_bins, y) else: self._histplot.setAxisScaleEngine(QwtPlot.yLeft, QwtLinearScaleEngine()) self._histplot.setAxisScale(QwtPlot.yLeft, 0, self._hist_max * (1 + self.ColorBarHeight)) self._histcurve1.setData(self._hist_bins, self._hist) self._histcurve2.setData(self._hist_bins, self._hist) if replot: self._histplot.replot()
class SequencePlayback(): def __init__(self): self.imageSequence = [] self.framerate = 0 self.currentFrame = -1 self.currentFrameIdx = -1 self.endFrameIdx = 1 self.state = '' self.timer = QTimer() self.timer.timerEvent = self.onTick Event.add(AudioPlaybackEvent.TICK, self.onTick) Event.add(PlaybackEvent.STATE, self.onState) self.setFramerate(24) pass def onState(self, state): self.state = state if state == PlayStateType.PLAY: self.play() elif state == PlayStateType.PAUSE: self.pause() pass pass def onTick(self, time): self.render() pass # def load(self, imagesPath=None): # if imagesPath: # for root, dirs, files in os.walk(imagesPath): # for filespath in files: # filename = os.path.join(root, filespath).replace('\\', '/') # # todo support image ext # if filename.find('.png') < 0: # continue # simage = SImage(filename) # self.imageSequence.append(simage) # simage.frameIdx = len(self.imageSequence) # self.endFrameIdx = simage.frameIdx # print('[load img]: ', filename) # Event.dis(ActionEvent.LOAD_SEQ, self.imageSequence) # pass def play(self): if not self.timer.isActive(): self.timer.start() pass pass def pause(self): if self.timer.isActive(): self.timer.stop() pass pass def render(self): self.currentFrameIdx = (self.currentFrameIdx + 1) % self.endFrameIdx event = SequencePlaybackEvent() event.type = SequencePlaybackEvent.RENDER_FRAME event.frameIdx = self.currentFrameIdx Event.dis(SequencePlaybackEvent.RENDER_FRAME, event) def setFramerate(self, framerate): self.framerate = framerate self.timer.setInterval(1000 / self.framerate) pass
class Comments(HTMLDisplay): # {{{ def __init__(self, parent=None): HTMLDisplay.__init__(self, parent) self.setAcceptDrops(False) self.setMaximumWidth(300) self.setMinimumWidth(300) self.wait_timer = QTimer(self) self.wait_timer.timeout.connect(self.update_wait) self.wait_timer.setInterval(800) self.dots_count = 0 self.anchor_clicked.connect(self.link_activated) def link_activated(self, url): from calibre.gui2 import open_url if url.scheme() in {'http', 'https'}: open_url(url) def show_wait(self): self.dots_count = 0 self.wait_timer.start() self.update_wait() def update_wait(self): self.dots_count += 1 self.dots_count %= 10 self.dots_count = self.dots_count or 1 self.setHtml( '<h2>'+_('Please wait')+ '<br><span id="dots">{}</span></h2>'.format('.' * self.dots_count)) def show_data(self, html): self.wait_timer.stop() def color_to_string(col): ans = '#000000' if col.isValid(): col = col.toRgb() if col.isValid(): ans = unicode_type(col.name()) return ans c = color_to_string(QApplication.palette().color(QPalette.Normal, QPalette.WindowText)) templ = '''\ <html> <head> <style type="text/css"> body, td {background-color: transparent; color: %s } a { text-decoration: none; } div.description { margin-top: 0; padding-top: 0; text-indent: 0 } table { margin-bottom: 0; padding-bottom: 0; } </style> </head> <body> <div class="description"> %%s </div> </body> <html> '''%(c,) self.setHtml(templ%html) def sizeHint(self): # This is needed, because on windows the dialog cannot be resized to # so that this widgets height become < sizeHint().height(). Qt sets the # sizeHint to (800, 600), which makes the dialog unusable on smaller # screens. return QSize(800, 300)
class LivestateQWidget(QWidget): """ Class who display items livestate: hosts, services and number of problems """ def __init__(self): super(LivestateQWidget, self).__init__() # Fields self.labels = { 'host': None, 'service': None, 'problem': None } self.timer = QTimer() def initialize(self): """ Initialize QWidget """ layout = QHBoxLayout() self.setLayout(layout) item_types = ['host', 'service', 'problem'] for item_type in item_types: item_widget = self.get_item_type_widget(item_type, 0, 0) layout.addWidget(item_widget) self.update_labels() update_livestate = int(settings.get_config('Alignak-app', 'update_livestate')) * 1000 self.timer.setInterval(update_livestate) self.timer.start() self.timer.timeout.connect(self.update_labels) def get_item_type_widget(self, item_type, problem_nb, total_nb): """ Create and return QWidget with backend data :param item_type: type of item: host, service, problem :type item_type: str :param problem_nb: number of problems for item type :type problem_nb: int :param total_nb: total number of item type :type total_nb: int :return: widget with its data :rtype: QWidget """ layout = QVBoxLayout() widget = QWidget() widget.setLayout(layout) problem_label = QLabel('%d' % problem_nb) problem_label.setObjectName('ok') problem_label.setToolTip( _('Number of unhandled %s problems') % ( item_type if 'problem' not in item_type else '' ) ) layout.addWidget(problem_label) layout.setAlignment(problem_label, Qt.AlignCenter) icon_label = QLabel() icon_label.setFixedSize(64, 64) icon_label.setScaledContents(True) layout.addWidget(icon_label) layout.setAlignment(icon_label, Qt.AlignCenter) total_label = QLabel('%d' % total_nb) total_label.setObjectName('total') total_label.setToolTip( _('Number of monitored %s') % ( item_type if 'problem' not in item_type else 'items' ) ) layout.addWidget(total_label) layout.setAlignment(total_label, Qt.AlignCenter) self.labels[item_type] = { 'problem': problem_label, 'icon': icon_label, 'total': total_label } return widget def update_labels(self): """ Update QLabels of QWidget """ items_and_problems = data_manager.get_items_and_problems() for item_type in self.labels: if items_and_problems[item_type]['problem'] < 1: self.labels[item_type]['problem'].setObjectName('ok') else: self.labels[item_type]['problem'].setObjectName('ko') self.labels[item_type]['problem'].style().unpolish(self.labels[item_type]['problem']) self.labels[item_type]['problem'].style().polish(self.labels[item_type]['problem']) self.labels[item_type]['problem'].update() self.labels[item_type]['problem'].setText( '%s' % str(items_and_problems[item_type]['problem']) ) self.labels[item_type]['icon'].setPixmap( get_icon_item(item_type, items_and_problems[item_type]['problem']) ) self.labels[item_type]['total'].setText( '%s' % str(items_and_problems[item_type]['total']) ) def paintEvent(self, _): """Override paintEvent to paint background""" opt = QStyleOption() opt.initFrom(self) painter = QPainter(self) self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
class MaterialManager(QObject): materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated. favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed def __init__(self, container_registry, parent = None): super().__init__(parent) self._application = Application.getInstance() self._container_registry = container_registry # type: ContainerRegistry # Material_type -> generic material metadata self._fallback_materials_map = dict() # type: Dict[str, Dict[str, Any]] # Root_material_id -> MaterialGroup self._material_group_map = dict() # type: Dict[str, MaterialGroup] # Approximate diameter str self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]] # We're using these two maps to convert between the specific diameter material id and the generic material id # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant # i.e. generic_pla -> generic_pla_175 # root_material_id -> approximate diameter str -> root_material_id for that diameter self._material_diameter_map = defaultdict(dict) # type: Dict[str, Dict[str, str]] # Material id including diameter (generic_pla_175) -> material root id (generic_pla) self._diameter_material_map = dict() # type: Dict[str, str] # This is used in Legacy UM3 send material function and the material management page. # GUID -> a list of material_groups self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]] # The machine definition ID for the non-machine-specific materials. # This is used as the last fallback option if the given machine-specific material(s) cannot be found. self._default_machine_definition_id = "fdmprinter" self._default_approximate_diameter_for_quality_search = "3" # When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't # want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't # react too many time. self._update_timer = QTimer(self) self._update_timer.setInterval(300) self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self._updateMaps) self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged) self._container_registry.containerAdded.connect(self._onContainerMetadataChanged) self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged) self._favorites = set() # type: Set[str] def initialize(self) -> None: # Find all materials and put them in a matrix for quick search. material_metadatas = {metadata["id"]: metadata for metadata in self._container_registry.findContainersMetadata(type = "material") if metadata.get("GUID")} # type: Dict[str, Dict[str, Any]] self._material_group_map = dict() # type: Dict[str, MaterialGroup] # Map #1 # root_material_id -> MaterialGroup for material_id, material_metadata in material_metadatas.items(): # We don't store empty material in the lookup tables if material_id == "empty_material": continue root_material_id = material_metadata.get("base_file", "") if root_material_id not in self._material_group_map: self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id])) self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id) group = self._material_group_map[root_material_id] # Store this material in the group of the appropriate root material. if material_id != root_material_id: new_node = MaterialNode(material_metadata) group.derived_material_node_list.append(new_node) # Order this map alphabetically so it's easier to navigate in a debugger self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0])) # Map #1.5 # GUID -> material group list self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]] for root_material_id, material_group in self._material_group_map.items(): guid = material_group.root_material_node.getMetaDataEntry("GUID", "") self._guid_material_groups_map[guid].append(material_group) # Map #2 # Lookup table for material type -> fallback material metadata, only for read-only materials grouped_by_type_dict = dict() # type: Dict[str, Any] material_types_without_fallback = set() for root_material_id, material_node in self._material_group_map.items(): material_type = material_node.root_material_node.getMetaDataEntry("material", "") if material_type not in grouped_by_type_dict: grouped_by_type_dict[material_type] = {"generic": None, "others": []} material_types_without_fallback.add(material_type) brand = material_node.root_material_node.getMetaDataEntry("brand", "") if brand.lower() == "generic": to_add = True if material_type in grouped_by_type_dict: diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "") if diameter != self._default_approximate_diameter_for_quality_search: to_add = False # don't add if it's not the default diameter if to_add: # Checking this first allow us to differentiate between not read only materials: # - if it's in the list, it means that is a new material without fallback # - if it is not, then it is a custom material with a fallback material (parent) if material_type in material_types_without_fallback: grouped_by_type_dict[material_type] = material_node.root_material_node._metadata material_types_without_fallback.remove(material_type) # Remove the materials that have no fallback materials for material_type in material_types_without_fallback: del grouped_by_type_dict[material_type] self._fallback_materials_map = grouped_by_type_dict # Map #3 # There can be multiple material profiles for the same material with different diameters, such as "generic_pla" # and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can # be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID # for quality search. self._material_diameter_map = defaultdict(dict) self._diameter_material_map = dict() # Group the material IDs by the same name, material, brand, and color but with different diameters. material_group_dict = dict() # type: Dict[Tuple[Any], Dict[str, str]] keys_to_fetch = ("name", "material", "brand", "color") for root_material_id, machine_node in self._material_group_map.items(): root_material_metadata = machine_node.root_material_node._metadata key_data_list = [] # type: List[Any] for key in keys_to_fetch: key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key)) key_data = cast(Tuple[Any], tuple(key_data_list)) # type: Tuple[Any] # If the key_data doesn't exist, it doesn't matter if the material is read only... if key_data not in material_group_dict: material_group_dict[key_data] = dict() else: # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it if not machine_node.is_read_only: continue approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "") material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "") # Map [root_material_id][diameter] -> root_material_id for this diameter for data_dict in material_group_dict.values(): for root_material_id1 in data_dict.values(): if root_material_id1 in self._material_diameter_map: continue diameter_map = data_dict for root_material_id2 in data_dict.values(): self._material_diameter_map[root_material_id2] = diameter_map default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search) if default_root_material_id is None: default_root_material_id = list(data_dict.values())[0] # no default diameter present, just take "the" only one for root_material_id in data_dict.values(): self._diameter_material_map[root_material_id] = default_root_material_id # Map #4 # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]] for material_metadata in material_metadatas.values(): self.__addMaterialMetadataIntoLookupTree(material_metadata) favorites = self._application.getPreferences().getValue("cura/favorite_materials") for item in favorites.split(";"): self._favorites.add(item) self.materialsUpdated.emit() def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None: material_id = material_metadata["id"] # We don't store empty material in the lookup tables if material_id == "empty_material": return root_material_id = material_metadata["base_file"] definition = material_metadata["definition"] approximate_diameter = material_metadata["approximate_diameter"] if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map: self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {} machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[ approximate_diameter] if definition not in machine_nozzle_buildplate_material_map: machine_nozzle_buildplate_material_map[definition] = MaterialNode() # This is a list of information regarding the intermediate nodes: # nozzle -> buildplate nozzle_name = material_metadata.get("variant_name") buildplate_name = material_metadata.get("buildplate_name") intermediate_node_info_list = [(nozzle_name, VariantType.NOZZLE), (buildplate_name, VariantType.BUILD_PLATE), ] variant_manager = self._application.getVariantManager() machine_node = machine_nozzle_buildplate_material_map[definition] current_node = machine_node current_intermediate_node_info_idx = 0 error_message = None # type: Optional[str] while current_intermediate_node_info_idx < len(intermediate_node_info_list): variant_name, variant_type = intermediate_node_info_list[current_intermediate_node_info_idx] if variant_name is not None: # The new material has a specific variant, so it needs to be added to that specific branch in the tree. variant = variant_manager.getVariantNode(definition, variant_name, variant_type) if variant is None: error_message = "Material {id} contains a variant {name} that does not exist.".format( id = material_metadata["id"], name = variant_name) break # Update the current node to advance to a more specific branch if variant_name not in current_node.children_map: current_node.children_map[variant_name] = MaterialNode() current_node = current_node.children_map[variant_name] current_intermediate_node_info_idx += 1 if error_message is not None: Logger.log("e", "%s It will not be added into the material lookup tree.", error_message) self._container_registry.addWrongContainerId(material_metadata["id"]) return # Add the material to the current tree node, which is the deepest (the most specific) branch we can find. # Sanity check: Make sure that there is no duplicated materials. if root_material_id in current_node.material_map: Logger.log("e", "Duplicated material [%s] with root ID [%s]. It has already been added.", material_id, root_material_id) ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id) return current_node.material_map[root_material_id] = MaterialNode(material_metadata) def _updateMaps(self): Logger.log("i", "Updating material lookup data ...") self.initialize() def _onContainerMetadataChanged(self, container): self._onContainerChanged(container) def _onContainerChanged(self, container): container_type = container.getMetaDataEntry("type") if container_type != "material": return # update the maps self._update_timer.start() def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]: return self._material_group_map.get(root_material_id) def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str: return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id) def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: return self._diameter_material_map.get(root_material_id, "") def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]: return self._guid_material_groups_map.get(guid) # # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str], buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter) return dict() machine_definition_id = machine_definition.getId() # If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id) nozzle_node = None buildplate_node = None if nozzle_name is not None and machine_node is not None: nozzle_node = machine_node.getChildNode(nozzle_name) # Get buildplate node if possible if nozzle_node is not None and buildplate_name is not None: buildplate_node = nozzle_node.getChildNode(buildplate_name) nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node] # Fallback mechanism of finding materials: # 1. buildplate-specific material # 2. nozzle-specific material # 3. machine-specific material # 4. generic material (for fdmprinter) machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", []) material_id_metadata_dict = dict() # type: Dict[str, MaterialNode] excluded_materials = set() for current_node in nodes_to_check: if current_node is None: continue # Only exclude the materials that are explicitly specified in the "exclude_materials" field. # Do not exclude other materials that are of the same type. for material_id, node in current_node.material_map.items(): if material_id in machine_exclude_materials: excluded_materials.add(material_id) continue if material_id not in material_id_metadata_dict: material_id_metadata_dict[material_id] = node if excluded_materials: Logger.log("d", "Exclude materials {excluded_materials} for machine {machine_definition_id}".format(excluded_materials = ", ".join(excluded_materials), machine_definition_id = machine_definition_id)) return material_id_metadata_dict # # A convenience function to get available materials for the given machine with the extruder position. # def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack", extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]: buildplate_name = machine.getBuildplateName() nozzle_name = None if extruder_stack.variant.getId() != "empty_variant": nozzle_name = extruder_stack.variant.getName() diameter = extruder_stack.approximateMaterialDiameter # Fetch the available materials (ContainerNode) for the current active machine and extruder setup. return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter) # # Gets MaterialNode for the given extruder and machine with the given material name. # Returns None if: # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str], buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]", diameter, rounded_diameter, root_material_id) return None # If there are nozzle materials, get the nozzle-specific material machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] # type: Dict[str, MaterialNode] machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) nozzle_node = None buildplate_node = None # Fallback for "fdmprinter" if the machine-specific materials cannot be found if machine_node is None: machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id) if machine_node is not None and nozzle_name is not None: nozzle_node = machine_node.getChildNode(nozzle_name) if nozzle_node is not None and buildplate_name is not None: buildplate_node = nozzle_node.getChildNode(buildplate_name) # Fallback mechanism of finding materials: # 1. buildplate-specific material # 2. nozzle-specific material # 3. machine-specific material # 4. generic material (for fdmprinter) nodes_to_check = [buildplate_node, nozzle_node, machine_node, machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)] material_node = None for node in nodes_to_check: if node is not None: material_node = node.material_map.get(root_material_id) if material_node: break return material_node # # Gets MaterialNode for the given extruder and machine with the given material type. # Returns None if: # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str, buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]: node = None machine_definition = global_stack.definition extruder_definition = global_stack.extruders[position].definition if parseBool(machine_definition.getMetaDataEntry("has_materials", False)): material_diameter = extruder_definition.getProperty("material_diameter", "value") if isinstance(material_diameter, SettingFunction): material_diameter = material_diameter(global_stack) # Look at the guid to material dictionary root_material_id = None for material_group in self._guid_material_groups_map[material_guid]: root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", "")) break if not root_material_id: Logger.log("i", "Cannot find materials with guid [%s] ", material_guid) return None node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name, material_diameter, root_material_id) return node # # Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla". # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use # the generic material IDs to search for qualities. # # An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its # extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine. # A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will # be "generic_pla". This function is intended to get a generic fallback material for the given material type. # # This function returns the generic root material ID for the given material type, where material types are "PLA", # "ABS", etc. # def getFallbackMaterialIdByMaterialType(self, material_type: str) -> Optional[str]: # For safety if material_type not in self._fallback_materials_map: Logger.log("w", "The material type [%s] does not have a fallback material" % material_type) return None fallback_material = self._fallback_materials_map[material_type] if fallback_material: return self.getRootMaterialIDWithoutDiameter(fallback_material["id"]) else: return None ## Get default material for given global stack, extruder position and extruder nozzle name # you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder) def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str], extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]: node = None buildplate_name = global_stack.getBuildplateName() machine_definition = global_stack.definition if extruder_definition is None: extruder_definition = global_stack.extruders[position].definition if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)): # At this point the extruder_definition is not None material_diameter = extruder_definition.getProperty("material_diameter", "value") if isinstance(material_diameter, SettingFunction): material_diameter = material_diameter(global_stack) approximate_material_diameter = str(round(material_diameter)) root_material_id = machine_definition.getMetaDataEntry("preferred_material") root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter) node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name, material_diameter, root_material_id) return node def removeMaterialByRootId(self, root_material_id: str): material_group = self.getMaterialGroup(root_material_id) if not material_group: Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id) return nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list for node in nodes_to_remove: self._container_registry.removeContainer(node.getMetaDataEntry("id", "")) # # Methods for GUI # # # Sets the new name for the given material. # @pyqtSlot("QVariant", str) def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: root_material_id = material_node.getMetaDataEntry("base_file") if root_material_id is None: return if self._container_registry.isReadOnly(root_material_id): Logger.log("w", "Cannot set name of read-only container %s.", root_material_id) return material_group = self.getMaterialGroup(root_material_id) if material_group: container = material_group.root_material_node.getContainer() if container: container.setName(name) # # Removes the given material. # @pyqtSlot("QVariant") def removeMaterial(self, material_node: "MaterialNode") -> None: root_material_id = material_node.getMetaDataEntry("base_file") if root_material_id is not None: self.removeMaterialByRootId(root_material_id) # # Creates a duplicate of a material, which has the same GUID and base_file metadata. # Returns the root material ID of the duplicated material if successful. # @pyqtSlot("QVariant", result = str) def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]: root_material_id = cast(str, material_node.getMetaDataEntry("base_file", "")) material_group = self.getMaterialGroup(root_material_id) if not material_group: Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id) return None base_container = material_group.root_material_node.getContainer() if not base_container: return None # Ensure all settings are saved. self._application.saveSettings() # Create a new ID & container to hold the data. new_containers = [] if new_base_id is None: new_base_id = self._container_registry.uniqueName(base_container.getId()) new_base_container = copy.deepcopy(base_container) new_base_container.getMetaData()["id"] = new_base_id new_base_container.getMetaData()["base_file"] = new_base_id if new_metadata is not None: for key, value in new_metadata.items(): new_base_container.getMetaData()[key] = value new_containers.append(new_base_container) # Clone all of them. for node in material_group.derived_material_node_list: container_to_copy = node.getContainer() if not container_to_copy: continue # Create unique IDs for every clone. new_id = new_base_id if container_to_copy.getMetaDataEntry("definition") != "fdmprinter": new_id += "_" + container_to_copy.getMetaDataEntry("definition") if container_to_copy.getMetaDataEntry("variant_name"): nozzle_name = container_to_copy.getMetaDataEntry("variant_name") new_id += "_" + nozzle_name.replace(" ", "_") new_container = copy.deepcopy(container_to_copy) new_container.getMetaData()["id"] = new_id new_container.getMetaData()["base_file"] = new_base_id if new_metadata is not None: for key, value in new_metadata.items(): new_container.getMetaData()[key] = value new_containers.append(new_container) for container_to_add in new_containers: container_to_add.setDirty(True) self._container_registry.addContainer(container_to_add) # if the duplicated material was favorite then the new material should also be added to favorite. if root_material_id in self.getFavorites(): self.addFavorite(new_base_id) return new_base_id # # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID. # Returns the ID of the newly created material. @pyqtSlot(result = str) def createMaterial(self) -> str: from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") # Ensure all settings are saved. self._application.saveSettings() machine_manager = self._application.getMachineManager() extruder_stack = machine_manager.activeStack approximate_diameter = str(extruder_stack.approximateMaterialDiameter) root_material_id = "generic_pla" root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter) material_group = self.getMaterialGroup(root_material_id) if not material_group: # This should never happen Logger.log("w", "Cannot get the material group of %s.", root_material_id) return "" # Create a new ID & container to hold the data. new_id = self._container_registry.uniqueName("custom_material") new_metadata = {"name": catalog.i18nc("@label", "Custom Material"), "brand": catalog.i18nc("@label", "Custom"), "GUID": str(uuid.uuid4()), } self.duplicateMaterial(material_group.root_material_node, new_base_id = new_id, new_metadata = new_metadata) return new_id @pyqtSlot(str) def addFavorite(self, root_material_id: str) -> None: self._favorites.add(root_material_id) self.materialsUpdated.emit() # Ensure all settings are saved. self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites))) self._application.saveSettings() @pyqtSlot(str) def removeFavorite(self, root_material_id: str) -> None: self._favorites.remove(root_material_id) self.materialsUpdated.emit() # Ensure all settings are saved. self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites))) self._application.saveSettings() @pyqtSlot() def getFavorites(self): return self._favorites
class SpyQWidget(QWidget): """ Class who create QWidget for spied hosts """ def __init__(self): super(SpyQWidget, self).__init__() self.host_services_lbl = QLabel(_('You are not spying on any hosts for now...')) self.spy_list_widget = SpyQListWidget() self.host_list_widget = QListWidget() self.spy_timer = QTimer() def initialize(self): """ Intialize Spy QWidget """ layout = QGridLayout() self.setLayout(layout) spy_icon = QLabel() spy_pixmap = QPixmap(settings.get_image('spy')) spy_icon.setPixmap(spy_pixmap) spy_icon.setScaledContents(True) spy_icon.setFixedSize(20, 20) layout.addWidget(spy_icon, 0, 0, 1, 1) layout.setAlignment(spy_icon, Qt.AlignRight) spy_title = QLabel(_('Spy Hosts')) spy_title.setObjectName('title') spy_title.setMinimumHeight(40) layout.addWidget(spy_title, 0, 1, 1, 1) hint_lbl = QLabel('Click to refresh, double-click to stop spying') hint_lbl.setObjectName('subtitle') layout.addWidget(hint_lbl, 1, 0, 1, 1) layout.setAlignment(hint_lbl, Qt.AlignCenter) self.host_services_lbl.setObjectName('subtitle') layout.addWidget(self.host_services_lbl, 1, 1, 1, 1) layout.setAlignment(self.host_services_lbl, Qt.AlignCenter) self.spy_list_widget.setDragDropMode(QAbstractItemView.DragDrop) self.spy_list_widget.setSelectionMode(QAbstractItemView.ExtendedSelection) self.spy_list_widget.doubleClicked.connect(self.remove_event) self.spy_list_widget.setAcceptDrops(True) self.spy_list_widget.setWordWrap(True) self.spy_list_widget.insertItem(0, self.get_hint_item()) self.spy_list_widget.item_dropped.connect(get_events_widget().remove_event) self.spy_list_widget.clicked.connect( lambda: self.manage_host_events(self.spy_list_widget.currentRow()) ) layout.addWidget(self.spy_list_widget, 2, 0, 1, 1) self.host_list_widget.setObjectName('spy') # self.host_list_widget.setMinimumWidth(500) layout.addWidget(self.host_list_widget, 2, 1, 1, 1) spy_interval = int(settings.get_config('Alignak-app', 'spy_interval')) * 1000 self.spy_timer.setInterval(spy_interval) self.spy_timer.start() self.spy_timer.timeout.connect(self.send_spy_events) @staticmethod def get_hint_item(): """ Return an EventItem with a hint text :return: event item with hint text :rtype: EventItem """ drop_hint_item = EventItem() drop_hint_item.setText(_('Drop host-related events here to spy on it...')) drop_hint_item.setIcon(QIcon(settings.get_image('spy'))) drop_hint_item.setFlags(Qt.ItemIsDropEnabled) return drop_hint_item def send_spy_events(self): """ Send event messages for all hosts who are spied """ if self.spy_list_widget.spied_hosts: for host_id in self.spy_list_widget.spied_hosts: host = data_manager.get_item('host', host_id) get_events_widget().add_event( EventItem.get_event_type(host.data), _('Host %s, current state: %s') % ( host.get_display_name(), host.data['ls_state']), host=host.item_id ) def manage_host_events(self, row): """ Manage spy events for a host, defined by current row of "spy_list_widget" :param row: current row of "spy_list_widget" :type row: int """ # Clear QListWidget self.host_list_widget.clear() # Get Host and its services if row < 0: item = None else: item = self.spy_list_widget.item(row) if item: host = data_manager.get_item('host', item.host) if _('(new !)') in item.data(Qt.DisplayRole): item.setData(Qt.DisplayRole, item.data(Qt.DisplayRole).replace(_('(new !)'), '')) item.setToolTip(item.toolTip().replace(_('(new !)'), '')) self.host_services_lbl.setText(_('Problems found for %s:') % host.get_display_name()) services = data_manager.get_host_services(host.item_id) if services: problems = False for service in services: if data_manager.is_problem('service', service.data): problems = True svc_state = _('Service %s is %s') % ( service.get_display_name(), service.data['ls_state'] ) event = EventItem() event.initialize( service.data['ls_state'], svc_state, host=host.item_id ) tooltip = 'Output: %s (%s)' % ( service.data['ls_output'], get_date_fromtimestamp(service.data['ls_last_check']) ) event.setToolTip(tooltip) self.host_list_widget.insertItem(0, event) if not problems: event = EventItem() event.initialize( host.data['ls_state'], _('%s is %s. Services of host seems managed.') % ( host.get_display_name(), host.data['ls_state']), host=host.item_id ) self.host_list_widget.insertItem(0, event) else: no_service_event = EventItem() no_service_event.initialize( host.data['ls_state'], _('%s is %s. No services.') % (host.get_display_name(), host.data['ls_state']) ) self.host_list_widget.insertItem(0, no_service_event) def remove_event(self): """ Remove item when user double click on an item, update parent tab text """ item = self.spy_list_widget.currentItem() self.spy_list_widget.spied_hosts.remove(item.data(Qt.UserRole)) self.spy_list_widget.takeItem(self.spy_list_widget.currentRow()) if not self.spy_list_widget.spied_hosts: self.host_list_widget.clear() self.spy_list_widget.insertItem(0, self.get_hint_item()) self.spy_list_widget.initialized = False self.host_services_lbl.setText(_('You are not spying on any hosts for now...')) if not self.spy_list_widget.selectedItems(): self.manage_host_events(self.spy_list_widget.currentRow()) self.update_parent_spytab() def update_parent_spytab(self): # pragma: no cover - not testable """ Update the parent spy tab text """ if self.parent(): if self.spy_list_widget.spied_hosts: self.parent().parent().setTabText( self.parent().parent().indexOf(self), _('Spied Hosts (%d)') % self.spy_list_widget.count() ) else: # Remove hint item from count self.parent().parent().setTabText( self.parent().parent().indexOf(self), _('Spied Hosts (%d)') % (self.spy_list_widget.count() - 1) )
class GridView(QListView): update_item = pyqtSignal(object) files_dropped = pyqtSignal(object) def __init__(self, parent): QListView.__init__(self, parent) setup_gestures(self) setup_dnd_interface(self) self.setUniformItemSizes(True) self.setWrapping(True) self.setFlow(self.LeftToRight) # We cannot set layout mode to batched, because that breaks # restore_vpos() # self.setLayoutMode(self.Batched) self.setResizeMode(self.Adjust) self.setSelectionMode(self.ExtendedSelection) self.setVerticalScrollMode(self.ScrollPerPixel) self.delegate = CoverDelegate(self) self.delegate.animation.valueChanged.connect( self.animation_value_changed) self.delegate.animation.finished.connect(self.animation_done) self.setItemDelegate(self.delegate) self.setSpacing(self.delegate.spacing) self.padding_left = 0 self.set_color() self.ignore_render_requests = Event() dpr = self.device_pixel_ratio self.thumbnail_cache = ThumbnailCache( max_size=gprefs['cover_grid_disk_cache_size'], thumbnail_size=(int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height()))) self.render_thread = None self.update_item.connect(self.re_render, type=Qt.QueuedConnection) self.doubleClicked.connect(self.double_clicked) self.setCursor(Qt.PointingHandCursor) self.gui = parent self.context_menu = None self.update_timer = QTimer(self) self.update_timer.setInterval(200) self.update_timer.timeout.connect(self.update_viewport) self.update_timer.setSingleShot(True) self.resize_timer = t = QTimer(self) t.setInterval(200), t.setSingleShot(True) t.timeout.connect(self.update_memory_cover_cache_size) def viewportEvent(self, ev): ret = gesture_viewport_event(self, ev) if ret is not None: return ret return QListView.viewportEvent(self, ev) @property def device_pixel_ratio(self): try: return self.devicePixelRatioF() except AttributeError: return self.devicePixelRatio() @property def first_visible_row(self): geom = self.viewport().geometry() for y in xrange(geom.top(), (self.spacing() * 2) + geom.top(), 5): for x in xrange(geom.left(), (self.spacing() * 2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: return ans @property def last_visible_row(self): geom = self.viewport().geometry() for y in xrange(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5): for x in xrange(geom.left(), (self.spacing() * 2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: item_width = self.delegate.item_size.width( ) + 2 * self.spacing() return ans + (geom.width() // item_width) def update_viewport(self): self.ignore_render_requests.clear() self.update_timer.stop() m = self.model() for r in xrange(self.first_visible_row or 0, self.last_visible_row or (m.count() - 1)): self.update(m.index(r, 0)) def double_clicked(self, index): d = self.delegate if d.animating is None and not config['disable_animations']: d.animating = index d.animation.start() if tweaks['doubleclick_on_library_view'] == 'open_viewer': self.gui.iactions['View'].view_triggered(index) elif tweaks['doubleclick_on_library_view'] in { 'edit_metadata', 'edit_cell' }: self.gui.iactions['Edit Metadata'].edit_metadata(False, False) def animation_value_changed(self, value): if self.delegate.animating is not None: self.update(self.delegate.animating) def animation_done(self): if self.delegate.animating is not None: idx = self.delegate.animating self.delegate.animating = None self.update(idx) def set_color(self): r, g, b = gprefs['cover_grid_color'] pal = QPalette() col = QColor(r, g, b) pal.setColor(pal.Base, col) tex = gprefs['cover_grid_texture'] if tex: from calibre.gui2.preferences.texture_chooser import texture_path path = texture_path(tex) if path: pm = QPixmap(path) if not pm.isNull(): val = pm.scaled(1, 1).toImage().pixel(0, 0) r, g, b = qRed(val), qGreen(val), qBlue(val) pal.setBrush(pal.Base, QBrush(pm)) dark = (r + g + b) / 3.0 < 128 pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black)) self.setPalette(pal) self.delegate.highlight_color = pal.color(pal.Text) def refresh_settings(self): size_changed = ( gprefs['cover_grid_width'] != self.delegate.original_width or gprefs['cover_grid_height'] != self.delegate.original_height) if (size_changed or gprefs['cover_grid_show_title'] != self.delegate.original_show_title or gprefs['show_emblems'] != self.delegate.original_show_emblems or gprefs['emblem_size'] != self.delegate.orginal_emblem_size or gprefs['emblem_position'] != self.delegate.orginal_emblem_position): self.delegate.set_dimensions() self.setSpacing(self.delegate.spacing) if size_changed: self.delegate.cover_cache.clear() if gprefs['cover_grid_spacing'] != self.delegate.original_spacing: self.delegate.calculate_spacing() self.setSpacing(self.delegate.spacing) self.set_color() if size_changed: dpr = self.device_pixel_ratio self.thumbnail_cache.set_thumbnail_size( int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height())) cs = gprefs['cover_grid_disk_cache_size'] if (cs * (1024**2)) != self.thumbnail_cache.max_size: self.thumbnail_cache.set_size(cs) self.update_memory_cover_cache_size() def resizeEvent(self, ev): self.resize_timer.start() return QListView.resizeEvent(self, ev) def update_memory_cover_cache_size(self): try: sz = self.delegate.item_size except AttributeError: return rows, cols = self.width() // sz.width(), self.height() // sz.height() num = (rows + 1) * (cols + 1) limit = max(100, num * max(2, gprefs['cover_grid_cache_size_multiple'])) if limit != self.delegate.cover_cache.limit: self.delegate.cover_cache.set_limit(limit) def shown(self): self.update_memory_cover_cache_size() if self.render_thread is None: self.thumbnail_cache.set_database(self.gui.current_db) self.render_thread = Thread(target=self.render_covers) self.render_thread.daemon = True self.render_thread.start() def render_covers(self): q = self.delegate.render_queue while True: book_id = q.get() try: if book_id is None: return if self.ignore_render_requests.is_set(): continue try: self.render_cover(book_id) except: import traceback traceback.print_exc() finally: q.task_done() def render_cover(self, book_id): if self.ignore_render_requests.is_set(): return tcdata, timestamp = self.thumbnail_cache[book_id] use_cache = False if timestamp is None: # Not in cache has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, 0) else: has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, timestamp) if has_cover and cdata is None: # The cached cover is fresh cdata = tcdata use_cache = True if has_cover: p = QImage() p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG') dpr = self.device_pixel_ratio p.setDevicePixelRatio(dpr) if p.isNull() and cdata is tcdata: # Invalid image in cache self.thumbnail_cache.invalidate((book_id, )) self.update_item.emit(book_id) return cdata = None if p.isNull() else p if not use_cache: # cache is stale if cdata is not None: width, height = p.width(), p.height() scaled, nwidth, nheight = fit_image( width, height, int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height())) if scaled: if self.ignore_render_requests.is_set(): return p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) p.setDevicePixelRatio(dpr) cdata = p # update cache if cdata is None: self.thumbnail_cache.invalidate((book_id, )) else: try: self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata)) except EncodeError as err: self.thumbnail_cache.invalidate((book_id, )) prints(err) except Exception: import traceback traceback.print_exc() elif tcdata is not None: # Cover was removed, but it exists in cache, remove from cache self.thumbnail_cache.invalidate((book_id, )) self.delegate.cover_cache.set(book_id, cdata) self.update_item.emit(book_id) def re_render(self, book_id): self.delegate.cover_cache.clear_staging() m = self.model() try: index = m.db.row(book_id) except (IndexError, ValueError, KeyError): return self.update(m.index(index, 0)) def shutdown(self): self.ignore_render_requests.set() self.delegate.render_queue.put(None) self.thumbnail_cache.shutdown() def set_database(self, newdb, stage=0): if stage == 0: self.ignore_render_requests.set() try: for x in (self.delegate.cover_cache, self.thumbnail_cache): self.model().db.new_api.remove_cover_cache(x) except AttributeError: pass # db is None for x in (self.delegate.cover_cache, self.thumbnail_cache): newdb.new_api.add_cover_cache(x) try: # Use a timeout so that if, for some reason, the render thread # gets stuck, we dont deadlock, future covers wont get # rendered, but this is better than a deadlock join_with_timeout(self.delegate.render_queue) except RuntimeError: print('Cover rendering thread is stuck!') finally: self.ignore_render_requests.clear() else: self.delegate.cover_cache.clear() def select_rows(self, rows): sel = QItemSelection() sm = self.selectionModel() m = self.model() # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda (i, x): i - x): group = list(map(operator.itemgetter(1), g)) sel.merge( QItemSelection(m.index(min(group), 0), m.index(max(group), 0)), sm.Select) sm.select(sel, sm.ClearAndSelect)
class MaterialManager(QObject): materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated. favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed def __init__(self, container_registry, parent = None): super().__init__(parent) self._application = Application.getInstance() self._container_registry = container_registry # type: ContainerRegistry # Material_type -> generic material metadata self._fallback_materials_map = dict() # type: Dict[str, Dict[str, Any]] # Root_material_id -> MaterialGroup self._material_group_map = dict() # type: Dict[str, MaterialGroup] # Approximate diameter str self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]] # We're using these two maps to convert between the specific diameter material id and the generic material id # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant # i.e. generic_pla -> generic_pla_175 # root_material_id -> approximate diameter str -> root_material_id for that diameter self._material_diameter_map = defaultdict(dict) # type: Dict[str, Dict[str, str]] # Material id including diameter (generic_pla_175) -> material root id (generic_pla) self._diameter_material_map = dict() # type: Dict[str, str] # This is used in Legacy UM3 send material function and the material management page. # GUID -> a list of material_groups self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]] # The machine definition ID for the non-machine-specific materials. # This is used as the last fallback option if the given machine-specific material(s) cannot be found. self._default_machine_definition_id = "fdmprinter" self._default_approximate_diameter_for_quality_search = "3" # When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't # want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't # react too many time. self._update_timer = QTimer(self) self._update_timer.setInterval(300) self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self._updateMaps) self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged) self._container_registry.containerAdded.connect(self._onContainerMetadataChanged) self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged) self._favorites = set() # type: Set[str] def initialize(self) -> None: # Find all materials and put them in a matrix for quick search. material_metadatas = {metadata["id"]: metadata for metadata in self._container_registry.findContainersMetadata(type = "material") if metadata.get("GUID")} # type: Dict[str, Dict[str, Any]] self._material_group_map = dict() # type: Dict[str, MaterialGroup] # Map #1 # root_material_id -> MaterialGroup for material_id, material_metadata in material_metadatas.items(): # We don't store empty material in the lookup tables if material_id == "empty_material": continue root_material_id = material_metadata.get("base_file", "") if root_material_id not in self._material_group_map: self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id])) self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id) group = self._material_group_map[root_material_id] # Store this material in the group of the appropriate root material. if material_id != root_material_id: new_node = MaterialNode(material_metadata) group.derived_material_node_list.append(new_node) # Order this map alphabetically so it's easier to navigate in a debugger self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0])) # Map #1.5 # GUID -> material group list self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]] for root_material_id, material_group in self._material_group_map.items(): guid = material_group.root_material_node.getMetaDataEntry("GUID", "") self._guid_material_groups_map[guid].append(material_group) # Map #2 # Lookup table for material type -> fallback material metadata, only for read-only materials grouped_by_type_dict = dict() # type: Dict[str, Any] material_types_without_fallback = set() for root_material_id, material_node in self._material_group_map.items(): material_type = material_node.root_material_node.getMetaDataEntry("material", "") if material_type not in grouped_by_type_dict: grouped_by_type_dict[material_type] = {"generic": None, "others": []} material_types_without_fallback.add(material_type) brand = material_node.root_material_node.getMetaDataEntry("brand", "") if brand.lower() == "generic": to_add = True if material_type in grouped_by_type_dict: diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "") if diameter != self._default_approximate_diameter_for_quality_search: to_add = False # don't add if it's not the default diameter if to_add: # Checking this first allow us to differentiate between not read only materials: # - if it's in the list, it means that is a new material without fallback # - if it is not, then it is a custom material with a fallback material (parent) if material_type in material_types_without_fallback: grouped_by_type_dict[material_type] = material_node.root_material_node._metadata material_types_without_fallback.remove(material_type) # Remove the materials that have no fallback materials for material_type in material_types_without_fallback: del grouped_by_type_dict[material_type] self._fallback_materials_map = grouped_by_type_dict # Map #3 # There can be multiple material profiles for the same material with different diameters, such as "generic_pla" # and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can # be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID # for quality search. self._material_diameter_map = defaultdict(dict) self._diameter_material_map = dict() # Group the material IDs by the same name, material, brand, and color but with different diameters. material_group_dict = dict() # type: Dict[Tuple[Any], Dict[str, str]] keys_to_fetch = ("name", "material", "brand", "color") for root_material_id, machine_node in self._material_group_map.items(): root_material_metadata = machine_node.root_material_node._metadata key_data_list = [] # type: List[Any] for key in keys_to_fetch: key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key)) key_data = cast(Tuple[Any], tuple(key_data_list)) # type: Tuple[Any] # If the key_data doesn't exist, it doesn't matter if the material is read only... if key_data not in material_group_dict: material_group_dict[key_data] = dict() else: # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it if not machine_node.is_read_only: continue approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "") material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "") # Map [root_material_id][diameter] -> root_material_id for this diameter for data_dict in material_group_dict.values(): for root_material_id1 in data_dict.values(): if root_material_id1 in self._material_diameter_map: continue diameter_map = data_dict for root_material_id2 in data_dict.values(): self._material_diameter_map[root_material_id2] = diameter_map default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search) if default_root_material_id is None: default_root_material_id = list(data_dict.values())[0] # no default diameter present, just take "the" only one for root_material_id in data_dict.values(): self._diameter_material_map[root_material_id] = default_root_material_id # Map #4 # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]] for material_metadata in material_metadatas.values(): self.__addMaterialMetadataIntoLookupTree(material_metadata) favorites = self._application.getPreferences().getValue("cura/favorite_materials") for item in favorites.split(";"): self._favorites.add(item) self.materialsUpdated.emit() def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None: material_id = material_metadata["id"] # We don't store empty material in the lookup tables if material_id == "empty_material": return root_material_id = material_metadata["base_file"] definition = material_metadata["definition"] approximate_diameter = material_metadata["approximate_diameter"] if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map: self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {} machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[ approximate_diameter] if definition not in machine_nozzle_buildplate_material_map: machine_nozzle_buildplate_material_map[definition] = MaterialNode() # This is a list of information regarding the intermediate nodes: # nozzle -> buildplate nozzle_name = material_metadata.get("variant_name") buildplate_name = material_metadata.get("buildplate_name") intermediate_node_info_list = [(nozzle_name, VariantType.NOZZLE), (buildplate_name, VariantType.BUILD_PLATE), ] variant_manager = self._application.getVariantManager() machine_node = machine_nozzle_buildplate_material_map[definition] current_node = machine_node current_intermediate_node_info_idx = 0 error_message = None # type: Optional[str] while current_intermediate_node_info_idx < len(intermediate_node_info_list): variant_name, variant_type = intermediate_node_info_list[current_intermediate_node_info_idx] if variant_name is not None: # The new material has a specific variant, so it needs to be added to that specific branch in the tree. variant = variant_manager.getVariantNode(definition, variant_name, variant_type) if variant is None: error_message = "Material {id} contains a variant {name} that does not exist.".format( id = material_metadata["id"], name = variant_name) break # Update the current node to advance to a more specific branch if variant_name not in current_node.children_map: current_node.children_map[variant_name] = MaterialNode() current_node = current_node.children_map[variant_name] current_intermediate_node_info_idx += 1 if error_message is not None: Logger.log("e", "%s It will not be added into the material lookup tree.", error_message) self._container_registry.addWrongContainerId(material_metadata["id"]) return # Add the material to the current tree node, which is the deepest (the most specific) branch we can find. # Sanity check: Make sure that there is no duplicated materials. if root_material_id in current_node.material_map: Logger.log("e", "Duplicated material [%s] with root ID [%s]. It has already been added.", material_id, root_material_id) ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id) return current_node.material_map[root_material_id] = MaterialNode(material_metadata) def _updateMaps(self): Logger.log("i", "Updating material lookup data ...") self.initialize() def _onContainerMetadataChanged(self, container): self._onContainerChanged(container) def _onContainerChanged(self, container): container_type = container.getMetaDataEntry("type") if container_type != "material": return # update the maps self._update_timer.start() def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]: return self._material_group_map.get(root_material_id) def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str: return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id) def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: return self._diameter_material_map.get(root_material_id, "") def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]: return self._guid_material_groups_map.get(guid) # # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str], buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter) return dict() machine_definition_id = machine_definition.getId() # If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id) nozzle_node = None buildplate_node = None if nozzle_name is not None and machine_node is not None: nozzle_node = machine_node.getChildNode(nozzle_name) # Get buildplate node if possible if nozzle_node is not None and buildplate_name is not None: buildplate_node = nozzle_node.getChildNode(buildplate_name) nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node] # Fallback mechanism of finding materials: # 1. buildplate-specific material # 2. nozzle-specific material # 3. machine-specific material # 4. generic material (for fdmprinter) machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", []) material_id_metadata_dict = dict() # type: Dict[str, MaterialNode] excluded_materials = set() for current_node in nodes_to_check: if current_node is None: continue # Only exclude the materials that are explicitly specified in the "exclude_materials" field. # Do not exclude other materials that are of the same type. for material_id, node in current_node.material_map.items(): if material_id in machine_exclude_materials: excluded_materials.add(material_id) continue if material_id not in material_id_metadata_dict: material_id_metadata_dict[material_id] = node if excluded_materials: Logger.log("d", "Exclude materials {excluded_materials} for machine {machine_definition_id}".format(excluded_materials = ", ".join(excluded_materials), machine_definition_id = machine_definition_id)) return material_id_metadata_dict # # A convenience function to get available materials for the given machine with the extruder position. # def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack", extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]: buildplate_name = machine.getBuildplateName() nozzle_name = None if extruder_stack.variant.getId() != "empty_variant": nozzle_name = extruder_stack.variant.getName() diameter = extruder_stack.getApproximateMaterialDiameter() # Fetch the available materials (ContainerNode) for the current active machine and extruder setup. return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter) # # Gets MaterialNode for the given extruder and machine with the given material name. # Returns None if: # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str], buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]", diameter, rounded_diameter, root_material_id) return None # If there are nozzle materials, get the nozzle-specific material machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] # type: Dict[str, MaterialNode] machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) nozzle_node = None buildplate_node = None # Fallback for "fdmprinter" if the machine-specific materials cannot be found if machine_node is None: machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id) if machine_node is not None and nozzle_name is not None: nozzle_node = machine_node.getChildNode(nozzle_name) if nozzle_node is not None and buildplate_name is not None: buildplate_node = nozzle_node.getChildNode(buildplate_name) # Fallback mechanism of finding materials: # 1. buildplate-specific material # 2. nozzle-specific material # 3. machine-specific material # 4. generic material (for fdmprinter) nodes_to_check = [buildplate_node, nozzle_node, machine_node, machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)] material_node = None for node in nodes_to_check: if node is not None: material_node = node.material_map.get(root_material_id) if material_node: break return material_node # # Gets MaterialNode for the given extruder and machine with the given material type. # Returns None if: # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str, buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]: node = None machine_definition = global_stack.definition extruder_definition = global_stack.extruders[position].definition if parseBool(machine_definition.getMetaDataEntry("has_materials", False)): material_diameter = extruder_definition.getProperty("material_diameter", "value") if isinstance(material_diameter, SettingFunction): material_diameter = material_diameter(global_stack) # Look at the guid to material dictionary root_material_id = None for material_group in self._guid_material_groups_map[material_guid]: root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", "")) break if not root_material_id: Logger.log("i", "Cannot find materials with guid [%s] ", material_guid) return None node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name, material_diameter, root_material_id) return node # There are 2 ways to get fallback materials; # - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material # - A fallback by GUID; If a material has been duplicated, it should also check if the original materials do have # a GUID. This should only be done if the material itself does not have a quality just yet. def getFallBackMaterialIdsByMaterial(self, material: "InstanceContainer") -> List[str]: results = [] # type: List[str] material_groups = self.getMaterialGroupListByGUID(material.getMetaDataEntry("GUID")) for material_group in material_groups: # type: ignore if material_group.name != material.getId(): # If the material in the group is read only, put it at the front of the list (since that is the most # likely one to get a result) if material_group.is_read_only: results.insert(0, material_group.name) else: results.append(material_group.name) fallback = self.getFallbackMaterialIdByMaterialType(material.getMetaDataEntry("material")) if fallback is not None: results.append(fallback) return results # # Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla". # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use # the generic material IDs to search for qualities. # # An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its # extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine. # A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will # be "generic_pla". This function is intended to get a generic fallback material for the given material type. # # This function returns the generic root material ID for the given material type, where material types are "PLA", # "ABS", etc. # def getFallbackMaterialIdByMaterialType(self, material_type: str) -> Optional[str]: # For safety if material_type not in self._fallback_materials_map: Logger.log("w", "The material type [%s] does not have a fallback material" % material_type) return None fallback_material = self._fallback_materials_map[material_type] if fallback_material: return self.getRootMaterialIDWithoutDiameter(fallback_material["id"]) else: return None ## Get default material for given global stack, extruder position and extruder nozzle name # you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder) def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str], extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]: node = None buildplate_name = global_stack.getBuildplateName() machine_definition = global_stack.definition # The extruder-compatible material diameter in the extruder definition may not be the correct value because # the user can change it in the definition_changes container. if extruder_definition is None: extruder_stack_or_definition = global_stack.extruders[position] is_extruder_stack = True else: extruder_stack_or_definition = extruder_definition is_extruder_stack = False if extruder_stack_or_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)): if is_extruder_stack: material_diameter = extruder_stack_or_definition.getCompatibleMaterialDiameter() else: material_diameter = extruder_stack_or_definition.getProperty("material_diameter", "value") if isinstance(material_diameter, SettingFunction): material_diameter = material_diameter(global_stack) approximate_material_diameter = str(round(material_diameter)) root_material_id = machine_definition.getMetaDataEntry("preferred_material") root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter) node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name, material_diameter, root_material_id) return node def removeMaterialByRootId(self, root_material_id: str): material_group = self.getMaterialGroup(root_material_id) if not material_group: Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id) return nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list for node in nodes_to_remove: self._container_registry.removeContainer(node.getMetaDataEntry("id", "")) # # Methods for GUI # # # Sets the new name for the given material. # @pyqtSlot("QVariant", str) def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: root_material_id = material_node.getMetaDataEntry("base_file") if root_material_id is None: return if self._container_registry.isReadOnly(root_material_id): Logger.log("w", "Cannot set name of read-only container %s.", root_material_id) return material_group = self.getMaterialGroup(root_material_id) if material_group: container = material_group.root_material_node.getContainer() if container: container.setName(name) # # Removes the given material. # @pyqtSlot("QVariant") def removeMaterial(self, material_node: "MaterialNode") -> None: root_material_id = material_node.getMetaDataEntry("base_file") if root_material_id is not None: self.removeMaterialByRootId(root_material_id) # # Creates a duplicate of a material, which has the same GUID and base_file metadata. # Returns the root material ID of the duplicated material if successful. # @pyqtSlot("QVariant", result = str) def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]: root_material_id = cast(str, material_node.getMetaDataEntry("base_file", "")) material_group = self.getMaterialGroup(root_material_id) if not material_group: Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id) return None base_container = material_group.root_material_node.getContainer() if not base_container: return None # Ensure all settings are saved. self._application.saveSettings() # Create a new ID & container to hold the data. new_containers = [] if new_base_id is None: new_base_id = self._container_registry.uniqueName(base_container.getId()) new_base_container = copy.deepcopy(base_container) new_base_container.getMetaData()["id"] = new_base_id new_base_container.getMetaData()["base_file"] = new_base_id if new_metadata is not None: for key, value in new_metadata.items(): new_base_container.getMetaData()[key] = value new_containers.append(new_base_container) # Clone all of them. for node in material_group.derived_material_node_list: container_to_copy = node.getContainer() if not container_to_copy: continue # Create unique IDs for every clone. new_id = new_base_id if container_to_copy.getMetaDataEntry("definition") != "fdmprinter": new_id += "_" + container_to_copy.getMetaDataEntry("definition") if container_to_copy.getMetaDataEntry("variant_name"): nozzle_name = container_to_copy.getMetaDataEntry("variant_name") new_id += "_" + nozzle_name.replace(" ", "_") new_container = copy.deepcopy(container_to_copy) new_container.getMetaData()["id"] = new_id new_container.getMetaData()["base_file"] = new_base_id if new_metadata is not None: for key, value in new_metadata.items(): new_container.getMetaData()[key] = value new_containers.append(new_container) for container_to_add in new_containers: container_to_add.setDirty(True) self._container_registry.addContainer(container_to_add) # if the duplicated material was favorite then the new material should also be added to favorite. if root_material_id in self.getFavorites(): self.addFavorite(new_base_id) return new_base_id # # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID. # Returns the ID of the newly created material. @pyqtSlot(result = str) def createMaterial(self) -> str: from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") # Ensure all settings are saved. self._application.saveSettings() machine_manager = self._application.getMachineManager() extruder_stack = machine_manager.activeStack machine_definition = self._application.getGlobalContainerStack().definition preferred_material = machine_definition.getMetaDataEntry("preferred_material") approximate_diameter = str(extruder_stack.approximateMaterialDiameter) root_material_id = preferred_material if preferred_material else "generic_pla" root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter) material_group = self.getMaterialGroup(root_material_id) if not material_group: # This should never happen Logger.log("w", "Cannot get the material group of %s.", root_material_id) return "" # Create a new ID & container to hold the data. new_id = self._container_registry.uniqueName("custom_material") new_metadata = {"name": catalog.i18nc("@label", "Custom Material"), "brand": catalog.i18nc("@label", "Custom"), "GUID": str(uuid.uuid4()), } self.duplicateMaterial(material_group.root_material_node, new_base_id = new_id, new_metadata = new_metadata) return new_id @pyqtSlot(str) def addFavorite(self, root_material_id: str) -> None: self._favorites.add(root_material_id) self.materialsUpdated.emit() # Ensure all settings are saved. self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites))) self._application.saveSettings() @pyqtSlot(str) def removeFavorite(self, root_material_id: str) -> None: self._favorites.remove(root_material_id) self.materialsUpdated.emit() # Ensure all settings are saved. self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites))) self._application.saveSettings() @pyqtSlot() def getFavorites(self): return self._favorites
class ProblemsQWidget(QWidget): """ Class who create Problems QWidget """ def __init__(self, parent=None): super(ProblemsQWidget, self).__init__(parent) self.setWindowIcon(QIcon(settings.get_image('icon'))) # Fields self.line_search = QLineEdit() self.problems_table = ProblemsQTableView() self.problems_title = QLabel() self.actions_widget = ActionsQWidget() self.spy_widget = None self.filter_hosts_btn = ToggleQWidgetButton() self.filter_services_btn = ToggleQWidgetButton() self.spy_btn = QPushButton() self.host_btn = QPushButton() self.refresh_timer = QTimer() def initialize(self, spy_widget): """ Initialize QWidget and set SpyQWidget :param spy_widget: instance of SpyQWidget to manage spy events :type spy_widget: alignak_app.qobjects.events.spy.SpyQWidget """ problem_layout = QVBoxLayout() problem_layout.setContentsMargins(5, 20, 5, 5) self.setLayout(problem_layout) self.spy_widget = spy_widget self.problems_title.setObjectName('title') problem_layout.addWidget(self.problems_title) problem_layout.addWidget(self.get_search_widget()) problem_layout.addWidget(self.get_btn_widget()) problem_layout.addWidget(self.problems_table) self.update_problems_data() update_problems = int(settings.get_config('Alignak-app', 'update_problems')) * 1000 self.refresh_timer.setInterval(update_problems) self.refresh_timer.start() self.refresh_timer.timeout.connect(self.update_problems_data) def get_current_user_role_item(self): """ Return current selected item by ``Qt.UserRole`` :return: current selected item or None :rtype: alignak_app.items.item.Item """ item = self.problems_table.model().data( self.problems_table.selectionModel().currentIndex(), Qt.UserRole ) return item def update_action_buttons(self): """ Update status of action buttons and set current item for ActionsQWidget """ # Get item by UserRole item = self.get_current_user_role_item() if item: # If the elements had been ack or downtimed, they would not be present self.actions_widget.acknowledge_btn.setEnabled(True) self.actions_widget.downtime_btn.setEnabled(True) self.actions_widget.item = item if 'service' in item.item_type: host_id = item.data['host'] else: host_id = item.item_id self.spy_btn.setEnabled( bool(host_id not in self.spy_widget.spy_list_widget.spied_hosts) ) self.host_btn.setEnabled(True) else: self.actions_widget.acknowledge_btn.setEnabled(False) self.actions_widget.downtime_btn.setEnabled(False) self.host_btn.setEnabled(False) self.spy_btn.setEnabled(False) def get_search_widget(self): """ Create and return the search QWidget :return: search QWidget :rtype: QWidget """ widget = QWidget() layout = QHBoxLayout() layout.setSpacing(0) layout.setContentsMargins(5, 20, 5, 10) widget.setLayout(layout) # Search label search_lbl = QLabel(_('Search Problems')) search_lbl.setObjectName('bordertitle') search_lbl.setFixedHeight(25) search_lbl.setToolTip(_('Search Problems')) layout.addWidget(search_lbl) # QLineEdit self.line_search.setFixedHeight(search_lbl.height()) self.line_search.setPlaceholderText(_('Type text to filter problems...')) layout.addWidget(self.line_search) # Refresh button refresh_btn = QPushButton(_('Refresh')) refresh_btn.setObjectName('ok') refresh_btn.setFixedSize(120, search_lbl.height()) refresh_btn.setToolTip(_('Refresh problems')) refresh_btn.clicked.connect(self.update_problems_data) layout.addWidget(refresh_btn) return widget def get_btn_widget(self): """ Return QWidget with spy and host synthesis QPushButtons :return: widget with spy and host button :rtype: QWidget """ widget_btn = QWidget() layout_btn = QHBoxLayout() layout_btn.setContentsMargins(0, 0, 0, 5) widget_btn.setLayout(layout_btn) host_filter = QLabel(_('Filter hosts')) host_filter.setObjectName('subtitle') layout_btn.addWidget(host_filter) self.filter_hosts_btn.initialize() self.filter_hosts_btn.update_btn_state(False) self.filter_hosts_btn.toggle_btn.clicked.connect(lambda: self.update_problems_data('host')) layout_btn.addWidget(self.filter_hosts_btn) service_filter = QLabel(_('Filter services')) service_filter.setObjectName('subtitle') layout_btn.addWidget(service_filter) self.filter_services_btn.initialize() self.filter_services_btn.update_btn_state(False) self.filter_services_btn.toggle_btn.clicked.connect( lambda: self.update_problems_data('service') ) layout_btn.addWidget(self.filter_services_btn) layout_btn.addStretch() self.host_btn.setIcon(QIcon(settings.get_image('host'))) self.host_btn.setFixedSize(80, 20) self.host_btn.setEnabled(False) self.host_btn.setToolTip(_('See current item in synthesis view')) layout_btn.addWidget(self.host_btn) self.spy_btn.setIcon(QIcon(settings.get_image('spy'))) self.spy_btn.setFixedSize(80, 20) self.spy_btn.setEnabled(False) self.spy_btn.setToolTip(_('Spy current host')) self.spy_btn.clicked.connect(self.add_spied_host) layout_btn.addWidget(self.spy_btn) self.actions_widget.initialize(None) self.actions_widget.acknowledge_btn.setEnabled(False) self.actions_widget.downtime_btn.setEnabled(False) layout_btn.addWidget(self.actions_widget) layout_btn.setAlignment(Qt.AlignCenter) return widget_btn def add_spied_host(self): """ Add a host to spied hosts """ # Get item by UserRole item = self.get_current_user_role_item() if item: if 'service' in item.item_type: item_id = item.data['host'] else: item_id = item.item_id app_backend.query_services(item_id) self.spy_widget.spy_list_widget.add_spy_host(item_id) self.spy_widget.update_parent_spytab() self.update_action_buttons() def update_problems_data(self, item_type=''): """ Update data of Problems QTableWidget and problems title :param item_type: type of item to filter :type item_type: str """ problems_data = data_manager.get_problems() old_research = self.line_search.text() if self.parent(): self.parent().parent().setTabText( self.parent().parent().indexOf(self), _("Problems (%d)") % len(problems_data['problems']) ) self.problems_title.setText( _('There are %d problems to manage (hosts: %d, services: %d)') % ( len(problems_data['problems']), problems_data['hosts_nb'], problems_data['services_nb'] ) ) if self.filter_hosts_btn.is_checked() and not self.filter_services_btn.is_checked(): item_type = 'host' if self.filter_services_btn.is_checked() and not self.filter_hosts_btn.is_checked(): item_type = 'service' if not self.filter_services_btn.is_checked() and not self.filter_hosts_btn.is_checked(): item_type = '' if isinstance(item_type, str): if 'host' in item_type and self.filter_hosts_btn.is_checked(): if self.filter_services_btn.is_checked(): self.filter_services_btn.update_btn_state(False) if 'service' in item_type and self.filter_services_btn.is_checked(): if self.filter_hosts_btn.is_checked(): self.filter_hosts_btn.update_btn_state(False) problems_data['problems'] = [ item for item in problems_data['problems'] if item_type in item.item_type ] proxy_filter = self.problems_table.update_view(problems_data) if problems_data['problems']: self.line_search.textChanged.connect(proxy_filter.setFilterRegExp) else: self.problems_title.setText(_('If problems are found, they will be displayed here.')) self.problems_table.selectionModel().selectionChanged.connect(self.update_action_buttons) self.update_action_buttons() if old_research: self.line_search.setText(old_research) self.line_search.textChanged.emit(old_research)
class GridView(QListView): update_item = pyqtSignal(object) files_dropped = pyqtSignal(object) def __init__(self, parent): QListView.__init__(self, parent) setup_gestures(self) setup_dnd_interface(self) self.setUniformItemSizes(True) self.setWrapping(True) self.setFlow(self.LeftToRight) # We cannot set layout mode to batched, because that breaks # restore_vpos() # self.setLayoutMode(self.Batched) self.setResizeMode(self.Adjust) self.setSelectionMode(self.ExtendedSelection) self.setVerticalScrollMode(self.ScrollPerPixel) self.delegate = CoverDelegate(self) self.delegate.animation.valueChanged.connect(self.animation_value_changed) self.delegate.animation.finished.connect(self.animation_done) self.setItemDelegate(self.delegate) self.setSpacing(self.delegate.spacing) self.padding_left = 0 self.set_color() self.ignore_render_requests = Event() dpr = self.device_pixel_ratio self.thumbnail_cache = ThumbnailCache(max_size=gprefs['cover_grid_disk_cache_size'], thumbnail_size=(int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height()))) self.render_thread = None self.update_item.connect(self.re_render, type=Qt.QueuedConnection) self.doubleClicked.connect(self.double_clicked) self.setCursor(Qt.PointingHandCursor) self.gui = parent self.context_menu = None self.update_timer = QTimer(self) self.update_timer.setInterval(200) self.update_timer.timeout.connect(self.update_viewport) self.update_timer.setSingleShot(True) self.resize_timer = t = QTimer(self) t.setInterval(200), t.setSingleShot(True) t.timeout.connect(self.update_memory_cover_cache_size) def viewportEvent(self, ev): ret = gesture_viewport_event(self, ev) if ret is not None: return ret return QListView.viewportEvent(self, ev) @property def device_pixel_ratio(self): try: return self.devicePixelRatioF() except AttributeError: return self.devicePixelRatio() @property def first_visible_row(self): geom = self.viewport().geometry() for y in xrange(geom.top(), (self.spacing()*2) + geom.top(), 5): for x in xrange(geom.left(), (self.spacing()*2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: return ans @property def last_visible_row(self): geom = self.viewport().geometry() for y in xrange(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5): for x in xrange(geom.left(), (self.spacing()*2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: item_width = self.delegate.item_size.width() + 2*self.spacing() return ans + (geom.width() // item_width) def update_viewport(self): self.ignore_render_requests.clear() self.update_timer.stop() m = self.model() for r in xrange(self.first_visible_row or 0, self.last_visible_row or (m.count() - 1)): self.update(m.index(r, 0)) def double_clicked(self, index): d = self.delegate if d.animating is None and not config['disable_animations']: d.animating = index d.animation.start() if tweaks['doubleclick_on_library_view'] == 'open_viewer': self.gui.iactions['View'].view_triggered(index) elif tweaks['doubleclick_on_library_view'] in {'edit_metadata', 'edit_cell'}: self.gui.iactions['Edit Metadata'].edit_metadata(False, False) def animation_value_changed(self, value): if self.delegate.animating is not None: self.update(self.delegate.animating) def animation_done(self): if self.delegate.animating is not None: idx = self.delegate.animating self.delegate.animating = None self.update(idx) def set_color(self): r, g, b = gprefs['cover_grid_color'] pal = QPalette() col = QColor(r, g, b) pal.setColor(pal.Base, col) tex = gprefs['cover_grid_texture'] if tex: from calibre.gui2.preferences.texture_chooser import texture_path path = texture_path(tex) if path: pm = QPixmap(path) if not pm.isNull(): val = pm.scaled(1, 1).toImage().pixel(0, 0) r, g, b = qRed(val), qGreen(val), qBlue(val) pal.setBrush(pal.Base, QBrush(pm)) dark = (r + g + b)/3.0 < 128 pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black)) self.setPalette(pal) self.delegate.highlight_color = pal.color(pal.Text) def refresh_settings(self): size_changed = ( gprefs['cover_grid_width'] != self.delegate.original_width or gprefs['cover_grid_height'] != self.delegate.original_height ) if (size_changed or gprefs['cover_grid_show_title'] != self.delegate.original_show_title or gprefs['show_emblems'] != self.delegate.original_show_emblems or gprefs['emblem_size'] != self.delegate.orginal_emblem_size or gprefs['emblem_position'] != self.delegate.orginal_emblem_position): self.delegate.set_dimensions() self.setSpacing(self.delegate.spacing) if size_changed: self.delegate.cover_cache.clear() if gprefs['cover_grid_spacing'] != self.delegate.original_spacing: self.delegate.calculate_spacing() self.setSpacing(self.delegate.spacing) self.set_color() if size_changed: dpr = self.device_pixel_ratio self.thumbnail_cache.set_thumbnail_size(int(dpr * self.delegate.cover_size.width()), int(dpr*self.delegate.cover_size.height())) cs = gprefs['cover_grid_disk_cache_size'] if (cs*(1024**2)) != self.thumbnail_cache.max_size: self.thumbnail_cache.set_size(cs) self.update_memory_cover_cache_size() def resizeEvent(self, ev): self.resize_timer.start() return QListView.resizeEvent(self, ev) def update_memory_cover_cache_size(self): try: sz = self.delegate.item_size except AttributeError: return rows, cols = self.width() // sz.width(), self.height() // sz.height() num = (rows + 1) * (cols + 1) limit = max(100, num * max(2, gprefs['cover_grid_cache_size_multiple'])) if limit != self.delegate.cover_cache.limit: self.delegate.cover_cache.set_limit(limit) def shown(self): self.update_memory_cover_cache_size() if self.render_thread is None: self.thumbnail_cache.set_database(self.gui.current_db) self.render_thread = Thread(target=self.render_covers) self.render_thread.daemon = True self.render_thread.start() def render_covers(self): q = self.delegate.render_queue while True: book_id = q.get() try: if book_id is None: return if self.ignore_render_requests.is_set(): continue try: self.render_cover(book_id) except: import traceback traceback.print_exc() finally: q.task_done() def render_cover(self, book_id): if self.ignore_render_requests.is_set(): return tcdata, timestamp = self.thumbnail_cache[book_id] use_cache = False if timestamp is None: # Not in cache has_cover, cdata, timestamp = self.model().db.new_api.cover_or_cache(book_id, 0) else: has_cover, cdata, timestamp = self.model().db.new_api.cover_or_cache(book_id, timestamp) if has_cover and cdata is None: # The cached cover is fresh cdata = tcdata use_cache = True if has_cover: p = QImage() p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG') dpr = self.device_pixel_ratio p.setDevicePixelRatio(dpr) if p.isNull() and cdata is tcdata: # Invalid image in cache self.thumbnail_cache.invalidate((book_id,)) self.update_item.emit(book_id) return cdata = None if p.isNull() else p if not use_cache: # cache is stale if cdata is not None: width, height = p.width(), p.height() scaled, nwidth, nheight = fit_image( width, height, int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height())) if scaled: if self.ignore_render_requests.is_set(): return p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) p.setDevicePixelRatio(dpr) cdata = p # update cache if cdata is None: self.thumbnail_cache.invalidate((book_id,)) else: try: self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata)) except EncodeError as err: self.thumbnail_cache.invalidate((book_id,)) prints(err) except Exception: import traceback traceback.print_exc() elif tcdata is not None: # Cover was removed, but it exists in cache, remove from cache self.thumbnail_cache.invalidate((book_id,)) self.delegate.cover_cache.set(book_id, cdata) self.update_item.emit(book_id) def re_render(self, book_id): self.delegate.cover_cache.clear_staging() m = self.model() try: index = m.db.row(book_id) except (IndexError, ValueError, KeyError): return self.update(m.index(index, 0)) def shutdown(self): self.ignore_render_requests.set() self.delegate.render_queue.put(None) self.thumbnail_cache.shutdown() def set_database(self, newdb, stage=0): if stage == 0: self.ignore_render_requests.set() try: for x in (self.delegate.cover_cache, self.thumbnail_cache): self.model().db.new_api.remove_cover_cache(x) except AttributeError: pass # db is None for x in (self.delegate.cover_cache, self.thumbnail_cache): newdb.new_api.add_cover_cache(x) try: # Use a timeout so that if, for some reason, the render thread # gets stuck, we dont deadlock, future covers wont get # rendered, but this is better than a deadlock join_with_timeout(self.delegate.render_queue) except RuntimeError: print ('Cover rendering thread is stuck!') finally: self.ignore_render_requests.clear() else: self.delegate.cover_cache.clear() def select_rows(self, rows): sel = QItemSelection() sm = self.selectionModel() m = self.model() # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda (i,x):i-x): group = list(map(operator.itemgetter(1), g)) sel.merge(QItemSelection(m.index(min(group), 0), m.index(max(group), 0)), sm.Select) sm.select(sel, sm.ClearAndSelect)
class AppTrayIcon(QSystemTrayIcon): """ Class who create `QMenu` and `QActions` for `QSystemTrayIcon` (displayed in task bar) """ def __init__(self, icon, parent=None): QSystemTrayIcon.__init__(self, icon, parent) # Fields self.menu = QMenu(parent) self.app_about = AboutQDialog() self.app_main = AppQMainWindow() self.connection_timer = QTimer() self.connection_nb = 3 self.tray_actions = { 'app': QAction(), 'webui': QAction(), 'reload': QAction(), 'about': QAction(), 'exit': QAction(), } def build_menu(self): """ Initialize and create each QAction of QMenu """ logger.info("Start TrayIcon...") self.connection_timer.setInterval(10000) self.connection_timer.start() self.connection_timer.timeout.connect(self.check_connection) # Create actions self.add_alignak_menu() self.add_webui_menu() self.menu.addSeparator() self.add_reload_menu() self.add_about_menu() self.menu.addSeparator() self.add_quit_menu() self.setContextMenu(self.menu) self.refresh_menus() def check_connection(self): """ Check periodically connection to Alignak backend """ if app_backend.connected: connect = app_backend.login(check=True) logger.info('App check connection: %s', app_backend.connection_status[connect]) self.connection_nb = 3 elif not app_backend.connected and self.connection_nb < 1: connect = app_backend.login(check=True) logger.warning('App check connection: %s', app_backend.connection_status[connect]) self.connection_nb = 3 elif not app_backend.connected: logger.warning('App check connection in %d0s', self.connection_nb) self.connection_nb -= 1 else: pass self.app_main.dock.status_widget.update_status() self.refresh_menus() def add_alignak_menu(self): """ Create and add to menu "app" QAction """ self.app_main.initialize() self.tray_actions['app'].setIcon(QIcon(settings.get_image('icon'))) self.tray_actions['app'].setText(_('Alignak-App')) self.tray_actions['app'].setToolTip(_('Display Alignak-App')) self.tray_actions['app'].triggered.connect(self.app_main.show) self.menu.addAction(self.tray_actions['app']) def add_webui_menu(self): """ Create and add to menu "webui" QAction """ self.tray_actions['webui'].setIcon(QIcon(settings.get_image('web'))) self.tray_actions['webui'].setText(_('Go to WebUI')) self.tray_actions['webui'].setToolTip(_('Go to Alignak WebUI')) self.tray_actions['webui'].triggered.connect( lambda: open_url('livestate') ) self.menu.addAction(self.tray_actions['webui']) def refresh_menus(self): """ Refresh menu if needed """ if settings.get_config('Alignak', 'webui'): self.tray_actions['webui'].setEnabled(True) else: self.tray_actions['webui'].setEnabled(False) def add_reload_menu(self): """ Create and add to menu "reload" QAction """ self.tray_actions['reload'].setIcon(QIcon(settings.get_image('refresh'))) self.tray_actions['reload'].setText(_('Reload configuration')) self.tray_actions['reload'].setToolTip(_('Reload configuration')) self.tray_actions['reload'].triggered.connect(self.reload_configuration) self.menu.addAction(self.tray_actions['reload']) def add_about_menu(self): """ Create and add to menu "about" QAction """ self.app_about.initialize() self.tray_actions['about'].setIcon(QIcon(settings.get_image('about'))) self.tray_actions['about'].setText(_('About...')) self.tray_actions['about'].setToolTip(_('About Alignak-app')) self.tray_actions['about'].triggered.connect(self.app_about.show_about) self.menu.addAction(self.tray_actions['about']) def add_quit_menu(self): """ Create and add to menu "exit" QAction """ self.tray_actions['exit'].setIcon(QIcon(settings.get_image('exit'))) self.tray_actions['exit'].setText(_('Quit')) self.tray_actions['exit'].setToolTip(_('Quit Alignak-app')) self.tray_actions['exit'].triggered.connect(self.quit_app) self.menu.addAction(self.tray_actions['exit']) @staticmethod def quit_app(): # pragma: no cover """ Quit application and close all widgets handle by application """ thread_manager.stop_threads() logger.info('----- Alignak-App STOP -----') sys.exit(0) @staticmethod def reload_configuration(): # pragma: no cover """ Reload configuration """ logger.info('Reload configuration...') settings.init_config() settings.init_css() send_event('INFO', _('Configuration reloaded'), timer=True)
class GridView(QListView): update_item = pyqtSignal(object) files_dropped = pyqtSignal(object) books_dropped = pyqtSignal(object) def __init__(self, parent): QListView.__init__(self, parent) self._ncols = None self.gesture_manager = GestureManager(self) setup_dnd_interface(self) self.setUniformItemSizes(True) self.setWrapping(True) self.setFlow(self.LeftToRight) # We cannot set layout mode to batched, because that breaks # restore_vpos() # self.setLayoutMode(self.Batched) self.setResizeMode(self.Adjust) self.setSelectionMode(self.ExtendedSelection) self.setVerticalScrollMode(self.ScrollPerPixel) self.delegate = CoverDelegate(self) self.delegate.animation.valueChanged.connect(self.animation_value_changed) self.delegate.animation.finished.connect(self.animation_done) self.setItemDelegate(self.delegate) self.setSpacing(self.delegate.spacing) self.set_color() self.ignore_render_requests = Event() dpr = self.device_pixel_ratio self.thumbnail_cache = ThumbnailCache(max_size=gprefs['cover_grid_disk_cache_size'], thumbnail_size=(int(dpr * self.delegate.cover_size.width()), int(dpr * self.delegate.cover_size.height()))) self.render_thread = None self.update_item.connect(self.re_render, type=Qt.QueuedConnection) self.doubleClicked.connect(self.double_clicked) self.setCursor(Qt.PointingHandCursor) self.gui = parent self.context_menu = None self.update_timer = QTimer(self) self.update_timer.setInterval(200) self.update_timer.timeout.connect(self.update_viewport) self.update_timer.setSingleShot(True) self.resize_timer = t = QTimer(self) t.setInterval(200), t.setSingleShot(True) t.timeout.connect(self.update_memory_cover_cache_size) def viewportEvent(self, ev): try: ret = self.gesture_manager.handle_event(ev) except AttributeError: ret = None if ret is not None: return ret return QListView.viewportEvent(self, ev) @property def device_pixel_ratio(self): try: return self.devicePixelRatioF() except AttributeError: return self.devicePixelRatio() @property def first_visible_row(self): geom = self.viewport().geometry() for y in range(geom.top(), (self.spacing()*2) + geom.top(), 5): for x in range(geom.left(), (self.spacing()*2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: return ans @property def last_visible_row(self): geom = self.viewport().geometry() for y in range(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5): for x in range(geom.left(), (self.spacing()*2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: item_width = self.delegate.item_size.width() + 2*self.spacing() return ans + (geom.width() // item_width) def update_viewport(self): self.ignore_render_requests.clear() self.update_timer.stop() m = self.model() for r in range(self.first_visible_row or 0, self.last_visible_row or (m.count() - 1)): self.update(m.index(r, 0)) def start_view_animation(self, index): d = self.delegate if d.animating is None and not config['disable_animations']: d.animating = index d.animation.start() def double_clicked(self, index): self.start_view_animation(index) if tweaks['doubleclick_on_library_view'] == 'open_viewer': self.gui.iactions['View'].view_triggered(index) elif tweaks['doubleclick_on_library_view'] in {'edit_metadata', 'edit_cell'}: self.gui.iactions['Edit Metadata'].edit_metadata(False, False) def animation_value_changed(self, value): if self.delegate.animating is not None: self.update(self.delegate.animating) def animation_done(self): if self.delegate.animating is not None: idx = self.delegate.animating self.delegate.animating = None self.update(idx) def set_color(self): r, g, b = gprefs['cover_grid_color'] pal = QPalette() col = QColor(r, g, b) pal.setColor(pal.Base, col) tex = gprefs['cover_grid_texture'] if tex: from calibre.gui2.preferences.texture_chooser import texture_path path = texture_path(tex) if path: pm = QPixmap(path) if not pm.isNull(): val = pm.scaled(1, 1).toImage().pixel(0, 0) r, g, b = qRed(val), qGreen(val), qBlue(val) pal.setBrush(pal.Base, QBrush(pm)) dark = (r + g + b)/3.0 < 128 pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black)) self.setPalette(pal) self.delegate.highlight_color = pal.color(pal.Text) def refresh_settings(self): size_changed = ( gprefs['cover_grid_width'] != self.delegate.original_width or gprefs['cover_grid_height'] != self.delegate.original_height ) if (size_changed or gprefs[ 'cover_grid_show_title'] != self.delegate.original_show_title or gprefs[ 'show_emblems'] != self.delegate.original_show_emblems or gprefs[ 'emblem_size'] != self.delegate.orginal_emblem_size or gprefs[ 'emblem_position'] != self.delegate.orginal_emblem_position): self.delegate.set_dimensions() self.setSpacing(self.delegate.spacing) if size_changed: self.delegate.cover_cache.clear() if gprefs['cover_grid_spacing'] != self.delegate.original_spacing: self.delegate.calculate_spacing() self.setSpacing(self.delegate.spacing) self.set_color() self.set_thumbnail_cache_image_size() cs = gprefs['cover_grid_disk_cache_size'] if (cs*(1024**2)) != self.thumbnail_cache.max_size: self.thumbnail_cache.set_size(cs) self.update_memory_cover_cache_size() def set_thumbnail_cache_image_size(self): dpr = self.device_pixel_ratio self.thumbnail_cache.set_thumbnail_size( int(dpr * self.delegate.cover_size.width()), int(dpr*self.delegate.cover_size.height())) def resizeEvent(self, ev): self._ncols = None self.resize_timer.start() return QListView.resizeEvent(self, ev) def update_memory_cover_cache_size(self): try: sz = self.delegate.item_size except AttributeError: return rows, cols = self.width() // sz.width(), self.height() // sz.height() num = (rows + 1) * (cols + 1) limit = max(100, num * max(2, gprefs['cover_grid_cache_size_multiple'])) if limit != self.delegate.cover_cache.limit: self.delegate.cover_cache.set_limit(limit) def shown(self): self.update_memory_cover_cache_size() if self.render_thread is None: self.thumbnail_cache.set_database(self.gui.current_db) self.render_thread = Thread(target=self.render_covers) self.render_thread.daemon = True self.render_thread.start() def render_covers(self): q = self.delegate.render_queue while True: book_id = q.get() try: if book_id is None: return if self.ignore_render_requests.is_set(): continue try: self.render_cover(book_id) except: import traceback traceback.print_exc() finally: q.task_done() def render_cover(self, book_id): if self.ignore_render_requests.is_set(): return dpr = self.device_pixel_ratio page_width = int(dpr * self.delegate.cover_size.width()) page_height = int(dpr * self.delegate.cover_size.height()) tcdata, timestamp = self.thumbnail_cache[book_id] use_cache = False if timestamp is None: # Not in cache has_cover, cdata, timestamp = self.model().db.new_api.cover_or_cache(book_id, 0) else: has_cover, cdata, timestamp = self.model().db.new_api.cover_or_cache(book_id, timestamp) if has_cover and cdata is None: # The cached cover is fresh cdata = tcdata use_cache = True if has_cover: p = QImage() p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG') p.setDevicePixelRatio(dpr) if p.isNull() and cdata is tcdata: # Invalid image in cache self.thumbnail_cache.invalidate((book_id,)) self.update_item.emit(book_id) return cdata = None if p.isNull() else p if not use_cache: # cache is stale if cdata is not None: width, height = p.width(), p.height() scaled, nwidth, nheight = fit_image( width, height, page_width, page_height) if scaled: if self.ignore_render_requests.is_set(): return p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) p.setDevicePixelRatio(dpr) cdata = p # update cache if cdata is None: self.thumbnail_cache.invalidate((book_id,)) else: try: self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata)) except EncodeError as err: self.thumbnail_cache.invalidate((book_id,)) prints(err) except Exception: import traceback traceback.print_exc() elif tcdata is not None: # Cover was removed, but it exists in cache, remove from cache self.thumbnail_cache.invalidate((book_id,)) self.delegate.cover_cache.set(book_id, cdata) self.update_item.emit(book_id) def re_render(self, book_id): self.delegate.cover_cache.clear_staging() m = self.model() try: index = m.db.row(book_id) except (IndexError, ValueError, KeyError): return self.update(m.index(index, 0)) def shutdown(self): self.ignore_render_requests.set() self.delegate.render_queue.put(None) self.thumbnail_cache.shutdown() def set_database(self, newdb, stage=0): if stage == 0: self.ignore_render_requests.set() try: for x in (self.delegate.cover_cache, self.thumbnail_cache): self.model().db.new_api.remove_cover_cache(x) except AttributeError: pass # db is None for x in (self.delegate.cover_cache, self.thumbnail_cache): newdb.new_api.add_cover_cache(x) try: # Use a timeout so that if, for some reason, the render thread # gets stuck, we dont deadlock, future covers wont get # rendered, but this is better than a deadlock join_with_timeout(self.delegate.render_queue) except RuntimeError: print('Cover rendering thread is stuck!') finally: self.ignore_render_requests.clear() else: self.delegate.cover_cache.clear() def select_rows(self, rows): sel = QItemSelection() sm = self.selectionModel() m = self.model() # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda i_x:i_x[0]-i_x[1]): group = list(map(operator.itemgetter(1), g)) sel.merge(QItemSelection(m.index(min(group), 0), m.index(max(group), 0)), sm.Select) sm.select(sel, sm.ClearAndSelect) def selectAll(self): # We re-implement this to ensure that only indexes from column 0 are # selected. The base class implementation selects all columns. This # causes problems with selection syncing, see # https://bugs.launchpad.net/bugs/1236348 m = self.model() sm = self.selectionModel() sel = QItemSelection(m.index(0, 0), m.index(m.rowCount(QModelIndex())-1, 0)) sm.select(sel, sm.ClearAndSelect) def set_current_row(self, row): sm = self.selectionModel() sm.setCurrentIndex(self.model().index(row, 0), sm.NoUpdate) def set_context_menu(self, menu): self.context_menu = menu def contextMenuEvent(self, event): if self.context_menu is None: return from calibre.gui2.main_window import clone_menu m = clone_menu(self.context_menu) if islinux else self.context_menu m.popup(event.globalPos()) event.accept() def get_selected_ids(self): m = self.model() return [m.id(i) for i in self.selectionModel().selectedIndexes()] def restore_vpos(self, vpos): self.verticalScrollBar().setValue(vpos) def restore_hpos(self, hpos): pass def handle_mouse_press_event(self, ev): if QApplication.keyboardModifiers() & Qt.ShiftModifier: # Shift-Click in QListView is broken. It selects extra items in # various circumstances, for example, click on some item in the # middle of a row then click on an item in the next row, all items # in the first row will be selected instead of only items after the # middle item. index = self.indexAt(ev.pos()) if not index.isValid(): return ci = self.currentIndex() sm = self.selectionModel() sm.setCurrentIndex(index, sm.NoUpdate) if not ci.isValid(): return if not sm.hasSelection(): sm.select(index, sm.ClearAndSelect) return cr = ci.row() tgt = index.row() top = self.model().index(min(cr, tgt), 0) bottom = self.model().index(max(cr, tgt), 0) sm.select(QItemSelection(top, bottom), sm.Select) else: return QListView.mousePressEvent(self, ev) def indices_for_merge(self, resolved=True): return self.selectionModel().selectedIndexes() def number_of_columns(self): # Number of columns currently visible in the grid if self._ncols is None: step = max(10, self.spacing()) for y in range(step, 500, step): for x in range(step, 500, step): i = self.indexAt(QPoint(x, y)) if i.isValid(): for x in range(self.viewport().width() - step, self.viewport().width() - 300, -step): j = self.indexAt(QPoint(x, y)) if j.isValid(): self._ncols = j.row() - i.row() + 1 return self._ncols return self._ncols def keyPressEvent(self, ev): if handle_enter_press(self, ev, self.start_view_animation, False): return k = ev.key() if ev.modifiers() & Qt.ShiftModifier and k in (Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down): ci = self.currentIndex() if not ci.isValid(): return c = ci.row() delta = {Qt.Key_Left: -1, Qt.Key_Right: 1, Qt.Key_Up: -self.number_of_columns(), Qt.Key_Down: self.number_of_columns()}[k] n = max(0, min(c + delta, self.model().rowCount(None) - 1)) if n == c: return sm = self.selectionModel() rows = {i.row() for i in sm.selectedIndexes()} if rows: mi, ma = min(rows), max(rows) end = mi if c == ma else ma if c == mi else c else: end = c top = self.model().index(min(n, end), 0) bottom = self.model().index(max(n, end), 0) sm.select(QItemSelection(top, bottom), sm.ClearAndSelect) sm.setCurrentIndex(self.model().index(n, 0), sm.NoUpdate) else: return QListView.keyPressEvent(self, ev) @property def current_book(self): ci = self.currentIndex() if ci.isValid(): try: return self.model().db.data.index_to_id(ci.row()) except (IndexError, ValueError, KeyError, TypeError, AttributeError): pass def current_book_state(self): return self.current_book def restore_current_book_state(self, state): book_id = state self.setFocus(Qt.OtherFocusReason) try: row = self.model().db.data.id_to_index(book_id) except (IndexError, ValueError, KeyError, TypeError, AttributeError): return self.set_current_row(row) self.select_rows((row,)) self.scrollTo(self.model().index(row, 0), self.PositionAtCenter) def marked_changed(self, old_marked, current_marked): changed = old_marked | current_marked m = self.model() for book_id in changed: try: self.update(m.index(m.db.data.id_to_index(book_id), 0)) except ValueError: pass def moveCursor(self, action, modifiers): index = QListView.moveCursor(self, action, modifiers) if action in (QListView.MoveLeft, QListView.MoveRight) and index.isValid(): ci = self.currentIndex() if ci.isValid() and index.row() == ci.row(): nr = index.row() + (1 if action == QListView.MoveRight else -1) if 0 <= nr < self.model().rowCount(QModelIndex()): index = self.model().index(nr, 0) return index def selectionCommand(self, index, event): if event and event.type() == event.KeyPress and event.key() in (Qt.Key_Home, Qt.Key_End) and event.modifiers() & Qt.CTRL: return QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows return super(GridView, self).selectionCommand(index, event) def wheelEvent(self, ev): if ev.phase() not in (Qt.ScrollUpdate, 0): return number_of_pixels = ev.pixelDelta() number_of_degrees = ev.angleDelta() / 8.0 b = self.verticalScrollBar() if number_of_pixels.isNull() or islinux: # pixelDelta() is broken on linux with wheel mice dy = number_of_degrees.y() / 15.0 # Scroll by approximately half a row dy = int(math.ceil((dy) * b.singleStep() / 2.0)) else: dy = number_of_pixels.y() if abs(dy) > 0: b.setValue(b.value() - dy) def paintEvent(self, ev): dpr = self.device_pixel_ratio page_width = int(dpr * self.delegate.cover_size.width()) page_height = int(dpr * self.delegate.cover_size.height()) size_changed = self.thumbnail_cache.set_thumbnail_size(page_width, page_height) if size_changed: self.delegate.cover_cache.clear() return super(GridView, self).paintEvent(ev)
class LiveCSS(QWidget): goto_declaration = pyqtSignal(object) def __init__(self, preview, parent=None): QWidget.__init__(self, parent) self.preview = preview self.preview_is_refreshing = False self.refresh_needed = False preview.refresh_starting.connect(self.preview_refresh_starting) preview.refreshed.connect(self.preview_refreshed) self.apply_theme() self.setAutoFillBackground(True) self.update_timer = QTimer(self) self.update_timer.timeout.connect(self.update_data) self.update_timer.setSingleShot(True) self.update_timer.setInterval(500) self.now_showing = (None, None, None) self.stack = s = QStackedLayout(self) self.setLayout(s) self.clear_label = la = QLabel( '<h3>' + _('No style information found') + '</h3><p>' + _('Move the cursor inside a HTML tag to see what styles' ' apply to that tag.')) la.setWordWrap(True) la.setAlignment(Qt.AlignTop | Qt.AlignLeft) s.addWidget(la) self.box = box = Box(self) box.hyperlink_activated.connect(self.goto_declaration, type=Qt.QueuedConnection) self.scroll = sc = QScrollArea(self) sc.setWidget(box) sc.setWidgetResizable(True) s.addWidget(sc) def preview_refresh_starting(self): self.preview_is_refreshing = True def preview_refreshed(self): self.preview_is_refreshing = False # We must let the event loop run otherwise the webview will return # stale data in read_data() self.refresh_needed = True self.start_update_timer() def apply_theme(self): f = self.font() f.setFamily(tprefs['editor_font_family'] or default_font_family()) f.setPointSize(tprefs['editor_font_size']) self.setFont(f) theme = get_theme(tprefs['editor_theme']) pal = self.palette() pal.setColor(pal.Window, theme_color(theme, 'Normal', 'bg')) pal.setColor(pal.WindowText, theme_color(theme, 'Normal', 'fg')) pal.setColor(pal.AlternateBase, theme_color(theme, 'HighlightRegion', 'bg')) pal.setColor(pal.Link, theme_color(theme, 'Link', 'fg')) pal.setColor(pal.LinkVisited, theme_color(theme, 'Keyword', 'fg')) self.setPalette(pal) if hasattr(self, 'box'): self.box.relayout() self.update() def clear(self): self.stack.setCurrentIndex(0) def show_data(self, editor_name, sourceline, tags): if self.preview_is_refreshing: return if sourceline is None: self.clear() else: data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: if editor_name == self.current_name and ( editor_name, sourceline, tags) == self.now_showing: # Try again in a little while in case there was a transient # error in the web view self.start_update_timer() return if self.now_showing == ( None, None, None) or self.now_showing[0] != self.current_name: self.clear() return # Try to refresh the data for the currently shown tag instead # of clearing editor_name, sourceline, tags = self.now_showing data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: self.clear() return self.now_showing = (editor_name, sourceline, tags) data['html_name'] = editor_name self.box.show_data(data) self.refresh_needed = False self.stack.setCurrentIndex(1) def read_data(self, sourceline, tags): mf = self.preview.view.page().mainFrame() tags = [x.lower() for x in tags] result = unicode_type( mf.evaluateJavaScript( 'window.calibre_preview_integration.live_css(%s, %s)' % (json.dumps(sourceline), json.dumps(tags))) or '') try: result = json.loads(result) except ValueError: result = None if result is not None: maximum_specificities = {} for node in result['nodes']: is_ancestor = node['is_ancestor'] for rule in node['css']: self.process_rule(rule, is_ancestor, maximum_specificities) for node in result['nodes']: for rule in node['css']: for prop in rule['properties']: if prop.specificity < maximum_specificities[prop.name]: prop.is_overriden = True return result def process_rule(self, rule, is_ancestor, maximum_specificities): selector = rule['selector'] sheet_index = rule['sheet_index'] rule_address = rule['rule_address'] or () if selector is not None: try: specificity = [0] + list(parse(selector)[0].specificity()) except (AttributeError, TypeError, SelectorError): specificity = [0, 0, 0, 0] else: # style attribute specificity = [1, 0, 0, 0] specificity.extend((sheet_index, tuple(rule_address))) ancestor_specificity = 0 if is_ancestor else 1 properties = [] for prop in rule['properties']: important = 1 if prop[-1] == 'important' else 0 p = Property(prop, [ancestor_specificity] + [important] + specificity) properties.append(p) if p.specificity > maximum_specificities.get( p.name, (0, 0, 0, 0, 0, 0)): maximum_specificities[p.name] = p.specificity rule['properties'] = properties href = rule['href'] if hasattr(href, 'startswith') and href.startswith( '%s://%s' % (FAKE_PROTOCOL, FAKE_HOST)): qurl = QUrl(href) name = qurl.path()[1:] if name: rule['href'] = name @property def current_name(self): return self.preview.current_name @property def is_visible(self): return self.isVisible() def showEvent(self, ev): self.update_timer.start() actions['auto-reload-preview'].setEnabled(True) return QWidget.showEvent(self, ev) def sync_to_editor(self): self.update_data() def update_data(self): if not self.is_visible or self.preview_is_refreshing: return editor_name = self.current_name ed = editors.get(editor_name, None) if self.update_timer.isActive() or (ed is None and editor_name is not None): return QTimer.singleShot(100, self.update_data) if ed is not None: sourceline, tags = ed.current_tag(for_position_sync=False) if self.refresh_needed or self.now_showing != (editor_name, sourceline, tags): self.show_data(editor_name, sourceline, tags) def start_update_timer(self): if self.is_visible: self.update_timer.start() def stop_update_timer(self): self.update_timer.stop() def navigate_to_declaration(self, data, editor): if data['type'] == 'inline': sourceline, tags = data['sourceline_address'] editor.goto_sourceline(sourceline, tags, attribute='style') elif data['type'] == 'sheet': editor.goto_css_rule(data['rule_address']) elif data['type'] == 'elem': editor.goto_css_rule(data['rule_address'], sourceline_address=data['sourceline_address'])