class CoverDelegate(QStyledItemDelegate): # {{{ needs_redraw = pyqtSignal() def __init__(self, parent): QStyledItemDelegate.__init__(self, parent) self.angle = 0 self.timer = QTimer(self) self.timer.timeout.connect(self.frame_changed) self.color = parent.palette().color(QPalette.WindowText) self.spinner_width = 64 def frame_changed(self, *args): self.angle = (self.angle + 30) % 360 self.needs_redraw.emit() def start_animation(self): self.angle = 0 self.timer.start(200) def stop_animation(self): self.timer.stop() def draw_spinner(self, painter, rect): width = rect.width() outer_radius = (width - 1) * 0.5 inner_radius = (width - 1) * 0.5 * 0.38 capsule_height = outer_radius - inner_radius capsule_width = int(capsule_height * (0.23 if width > 32 else 0.35)) capsule_radius = capsule_width // 2 painter.save() painter.setRenderHint(painter.Antialiasing) for i in xrange(12): color = QColor(self.color) color.setAlphaF(1.0 - (i / 12.0)) painter.setPen(Qt.NoPen) painter.setBrush(color) painter.save() painter.translate(rect.center()) painter.rotate(self.angle - i * 30.0) painter.drawRoundedRect(-capsule_width * 0.5, -(inner_radius + capsule_height), capsule_width, capsule_height, capsule_radius, capsule_radius) painter.restore() painter.restore() def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, index) style = QApplication.style() waiting = self.timer.isActive() and index.data(Qt.UserRole).toBool() if waiting: rect = QRect(0, 0, self.spinner_width, self.spinner_width) rect.moveCenter(option.rect.center()) self.draw_spinner(painter, rect) else: # Ensure the cover is rendered over any selection rect style.drawItemPixmap(painter, option.rect, Qt.AlignTop | Qt.AlignHCenter, QPixmap(index.data(Qt.DecorationRole)))
class CoverDelegate(QStyledItemDelegate): # {{{ needs_redraw = pyqtSignal() def __init__(self, parent): QStyledItemDelegate.__init__(self, parent) self.angle = 0 self.timer = QTimer(self) self.timer.timeout.connect(self.frame_changed) self.color = parent.palette().color(QPalette.WindowText) self.spinner_width = 64 def frame_changed(self, *args): self.angle = (self.angle+30)%360 self.needs_redraw.emit() def start_animation(self): self.angle = 0 self.timer.start(200) def stop_animation(self): self.timer.stop() def draw_spinner(self, painter, rect): width = rect.width() outer_radius = (width-1)*0.5 inner_radius = (width-1)*0.5*0.38 capsule_height = outer_radius - inner_radius capsule_width = int(capsule_height * (0.23 if width > 32 else 0.35)) capsule_radius = capsule_width//2 painter.save() painter.setRenderHint(painter.Antialiasing) for i in xrange(12): color = QColor(self.color) color.setAlphaF(1.0 - (i/12.0)) painter.setPen(Qt.NoPen) painter.setBrush(color) painter.save() painter.translate(rect.center()) painter.rotate(self.angle - i*30.0) painter.drawRoundedRect(-capsule_width*0.5, -(inner_radius+capsule_height), capsule_width, capsule_height, capsule_radius, capsule_radius) painter.restore() painter.restore() def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, index) style = QApplication.style() waiting = self.timer.isActive() and index.data(Qt.UserRole).toBool() if waiting: rect = QRect(0, 0, self.spinner_width, self.spinner_width) rect.moveCenter(option.rect.center()) self.draw_spinner(painter, rect) else: # Ensure the cover is rendered over any selection rect style.drawItemPixmap(painter, option.rect, Qt.AlignTop|Qt.AlignHCenter, QPixmap(index.data(Qt.DecorationRole)))
class Preview(QWidget): sync_requested = pyqtSignal(object, object) split_requested = pyqtSignal(object, object, object) split_start_requested = pyqtSignal() link_clicked = pyqtSignal(object, object) def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QVBoxLayout() self.setLayout(l) l.setContentsMargins(0, 0, 0, 0) self.view = WebView(self) self.view.page().sync_requested.connect(self.request_sync) self.view.page().split_requested.connect(self.request_split) self.view.page().loadFinished.connect(self.load_finished) self.inspector = self.view.inspector self.inspector.setPage(self.view.page()) l.addWidget(self.view) self.bar = QToolBar(self) l.addWidget(self.bar) ac = actions['auto-reload-preview'] ac.setCheckable(True) ac.setChecked(True) ac.toggled.connect(self.auto_reload_toggled) self.auto_reload_toggled(ac.isChecked()) self.bar.addAction(ac) ac = actions['sync-preview-to-editor'] ac.setCheckable(True) ac.setChecked(True) ac.toggled.connect(self.sync_toggled) self.sync_toggled(ac.isChecked()) self.bar.addAction(ac) self.bar.addSeparator() ac = actions['split-in-preview'] ac.setCheckable(True) ac.setChecked(False) ac.toggled.connect(self.split_toggled) self.split_toggled(ac.isChecked()) self.bar.addAction(ac) ac = actions['reload-preview'] ac.triggered.connect(self.refresh) self.bar.addAction(ac) actions['preview-dock'].toggled.connect(self.visibility_changed) self.current_name = None self.last_sync_request = None self.refresh_timer = QTimer(self) self.refresh_timer.timeout.connect(self.refresh) parse_worker.start() self.current_sync_request = None self.search = HistoryLineEdit2(self) self.search.initialize('tweak_book_preview_search') self.search.setPlaceholderText(_('Search in preview')) self.search.returnPressed.connect(partial(self.find, 'next')) self.bar.addSeparator() self.bar.addWidget(self.search) for d in ('next', 'prev'): ac = actions['find-%s-preview' % d] ac.triggered.connect(partial(self.find, d)) self.bar.addAction(ac) def find(self, direction): text = unicode(self.search.text()) self.view.findText(text, QWebPage.FindWrapsAroundDocument | ( QWebPage.FindBackward if direction == 'prev' else QWebPage.FindFlags(0))) def request_sync(self, tagname, href, lnum): if self.current_name: c = current_container() if tagname == 'a' and href: if href and href.startswith('#'): name = self.current_name else: name = c.href_to_name(href, self.current_name) if href else None if name == self.current_name: return self.view.page().go_to_anchor(urlparse(href).fragment, lnum) if name and c.exists(name) and c.mime_map[name] in OEB_DOCS: return self.link_clicked.emit(name, urlparse(href).fragment or TOP) self.sync_requested.emit(self.current_name, lnum) def request_split(self, loc, totals): if self.current_name: self.split_requested.emit(self.current_name, loc, totals) def sync_to_editor(self, name, lnum): self.current_sync_request = (name, lnum) QTimer.singleShot(100, self._sync_to_editor) def _sync_to_editor(self): if not actions['sync-preview-to-editor'].isChecked(): return try: if self.refresh_timer.isActive() or self.current_sync_request[0] != self.current_name: return QTimer.singleShot(100, self._sync_to_editor) except TypeError: return # Happens if current_sync_request is None lnum = self.current_sync_request[1] self.current_sync_request = None self.view.page().go_to_line(lnum) def show(self, name): if name != self.current_name: self.refresh_timer.stop() self.current_name = name parse_worker.add_request(name) self.view.setUrl(QUrl.fromLocalFile(current_container().name_to_abspath(name))) return True def refresh(self): if self.current_name: self.refresh_timer.stop() # This will check if the current html has changed in its editor, # and re-parse it if so parse_worker.add_request(self.current_name) # Tell webkit to reload all html and associated resources current_url = QUrl.fromLocalFile(current_container().name_to_abspath(self.current_name)) if current_url != self.view.url(): # The container was changed self.view.setUrl(current_url) else: self.view.refresh() def clear(self): self.view.clear() self.current_name = None @property def is_visible(self): return actions['preview-dock'].isChecked() def start_refresh_timer(self): if self.is_visible and actions['auto-reload-preview'].isChecked(): self.refresh_timer.start(tprefs['preview_refresh_time'] * 1000) def stop_refresh_timer(self): self.refresh_timer.stop() def auto_reload_toggled(self, checked): actions['auto-reload-preview'].setToolTip(_( 'Auto reload preview when text changes in editor') if not checked else _( 'Disable auto reload of preview')) def sync_toggled(self, checked): actions['sync-preview-to-editor'].setToolTip(_( 'Disable syncing of preview position to editor position') if checked else _( 'Enable syncing of preview position to editor position')) def visibility_changed(self, is_visible): if is_visible: self.refresh() def split_toggled(self, checked): actions['split-in-preview'].setToolTip(textwrap.fill(_( 'Abort file split') if checked else _( 'Split this file at a specified location.\n\nAfter clicking this button, click' ' inside the preview panel above at the location you want the file to be split.'))) if checked: self.split_start_requested.emit() else: self.view.page().split_mode(False) def do_start_split(self): self.view.page().split_mode(True) def stop_split(self): actions['split-in-preview'].setChecked(False) def load_finished(self, ok): if actions['split-in-preview'].isChecked(): if ok: self.do_start_split() else: self.stop_split() def apply_settings(self): s = self.view.page().settings() s.setFontSize(s.DefaultFontSize, tprefs['preview_base_font_size']) s.setFontSize(s.DefaultFixedFontSize, tprefs['preview_mono_font_size']) s.setFontSize(s.MinimumLogicalFontSize, tprefs['preview_minimum_font_size']) s.setFontSize(s.MinimumFontSize, tprefs['preview_minimum_font_size']) sf, ssf, mf = tprefs['preview_serif_family'], tprefs['preview_sans_family'], tprefs['preview_mono_family'] s.setFontFamily(s.StandardFont, {'serif':sf, 'sans':ssf, 'mono':mf, None:sf}[tprefs['preview_standard_font_family']]) s.setFontFamily(s.SerifFont, sf) s.setFontFamily(s.SansSerifFont, ssf) s.setFontFamily(s.FixedFont, mf)
class ScudCloud(QtGui.QMainWindow): forceClose = False messages = 0 speller = Speller() title = 'ScudCloud' def __init__(self, debug = False, parent = None, minimized = None, settings_path = ""): super(ScudCloud, self).__init__(parent) self.debug = debug self.minimized = minimized self.setWindowTitle(self.title) self.settings_path = settings_path self.notifier = Notifier(Resources.APP_NAME, Resources.get_path('scudcloud.png')) self.settings = QSettings(self.settings_path + '/scudcloud.cfg', QSettings.IniFormat) 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.leftPane = LeftPane(self) self.stackedWidget = QtGui.QStackedWidget() centralWidget = QtGui.QWidget(self) layout = QtGui.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 and above sessionBus.add_match_string("type='signal',interface='com.ubuntu.Upstart0_6'") 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.page().networkAccessManager().setCookieJar(self.cookiesjar) webView.page().networkAccessManager().setCache(self.diskCache) webView.load(QtCore.QUrl(url)) webView.show() self.stackedWidget.addWidget(webView) self.stackedWidget.setCurrentWidget(webView) 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.settings_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) 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", "False") else: self.settings.setValue("Menu", "True") 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 addMenu(self): # We'll register the webpage shorcuts with the window too (Fixes #338) undo = self.current().pageAction(QtWebKit.QWebPage.Undo) redo = self.current().pageAction(QtWebKit.QWebPage.Redo) cut = self.current().pageAction(QtWebKit.QWebPage.Cut) copy = self.current().pageAction(QtWebKit.QWebPage.Copy) paste = self.current().pageAction(QtWebKit.QWebPage.Paste) back = self.current().pageAction(QtWebKit.QWebPage.Back) forward = self.current().pageAction(QtWebKit.QWebPage.Forward) reload = self.current().pageAction(QtWebKit.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.switchTo(Resources.SIGNIN_URL)), "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(QtWebKit.QWebPage.Undo), undo.shortcut()), "redo": self.createAction(redo.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Redo), redo.shortcut()), "cut": self.createAction(cut.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Cut), cut.shortcut()), "copy": self.createAction(copy.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Copy), copy.shortcut()), "paste": self.createAction(paste.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Paste), paste.shortcut()), "back": self.createAction(back.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Back), back.shortcut()), "forward": self.createAction(forward.text(), lambda : self.current().page().triggerAction(QtWebKit.QWebPage.Forward), forward.shortcut()), "reload": self.createAction(reload.text(), lambda : self.current().page().triggerAction(QtWebKit.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 = QtGui.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) 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 = QtGui.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 QtGui.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() else: self.cookiesjar.save() self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) self.forceClose = False def show(self): self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) self.activateWindow() self.setVisible(True) def exit(self): 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 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() 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
class ScudCloud(QtGui.QMainWindow): forceClose = False messages = 0 speller = Speller() title = 'ScudCloud' def __init__(self, debug=False, parent=None, minimized=None, urgent_hint=None, settings_path=""): super(ScudCloud, self).__init__(parent) self.debug = debug self.minimized = minimized self.urgent_hint = urgent_hint self.setWindowTitle(self.title) self.settings_path = settings_path self.notifier = Notifier(Resources.APP_NAME, Resources.get_path('scudcloud.png')) self.settings = QSettings(self.settings_path + '/scudcloud.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.leftPane = LeftPane(self) self.stackedWidget = QtGui.QStackedWidget() centralWidget = QtGui.QWidget(self) layout = QtGui.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 and above sessionBus.add_match_string( "type='signal',interface='com.ubuntu.Upstart0_6'") 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.page().networkAccessManager().setCookieJar(self.cookiesjar) webView.page().networkAccessManager().setCache(self.diskCache) webView.load(QtCore.QUrl(url)) webView.show() self.stackedWidget.addWidget(webView) self.stackedWidget.setCurrentWidget(webView) 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.settings_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) 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", "False") else: self.settings.setValue("Menu", "True") 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(QtWebKit.QWebPage.Undo) redo = self.current().pageAction(QtWebKit.QWebPage.Redo) cut = self.current().pageAction(QtWebKit.QWebPage.Cut) copy = self.current().pageAction(QtWebKit.QWebPage.Copy) paste = self.current().pageAction(QtWebKit.QWebPage.Paste) back = self.current().pageAction(QtWebKit.QWebPage.Back) forward = self.current().pageAction(QtWebKit.QWebPage.Forward) reload = self.current().pageAction(QtWebKit.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( QtWebKit.QWebPage.Undo), undo.shortcut()), "redo": self.createAction( redo.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Redo), redo.shortcut()), "cut": self.createAction( cut.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Cut), cut.shortcut()), "copy": self.createAction( copy.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Copy), copy.shortcut()), "paste": self.createAction( paste.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Paste), paste.shortcut()), "back": self.createAction( back.text(), lambda: self.current().page().triggerAction( QtWebKit.QWebPage.Back), back.shortcut()), "forward": self.createAction( forward.text(), lambda: self.current().page() .triggerAction(QtWebKit.QWebPage.Forward), forward.shortcut()), "reload": self.createAction( reload.text(), lambda: self.current().page().triggerAction( QtWebKit.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 = QtGui.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) 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 = QtGui.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 QtGui.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() else: self.cookiesjar.save() self.settings.setValue("Domain", self.domains) self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) self.forceClose = False def show(self): self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) self.activateWindow() self.setVisible(True) def exit(self): 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 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
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 Preview(QWidget): sync_requested = pyqtSignal(object, object) split_requested = pyqtSignal(object, object) split_start_requested = pyqtSignal() def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QVBoxLayout() self.setLayout(l) l.setContentsMargins(0, 0, 0, 0) self.view = WebView(self) self.view.page().sync_requested.connect(self.request_sync) self.view.page().split_requested.connect(self.request_split) self.view.page().loadFinished.connect(self.load_finished) self.inspector = self.view.inspector self.inspector.setPage(self.view.page()) l.addWidget(self.view) self.bar = QToolBar(self) l.addWidget(self.bar) ac = actions['auto-reload-preview'] ac.setCheckable(True) ac.setChecked(True) ac.toggled.connect(self.auto_reload_toggled) self.auto_reload_toggled(ac.isChecked()) self.bar.addAction(ac) ac = actions['sync-preview-to-editor'] ac.setCheckable(True) ac.setChecked(True) ac.toggled.connect(self.sync_toggled) self.sync_toggled(ac.isChecked()) self.bar.addAction(ac) self.bar.addSeparator() ac = actions['split-in-preview'] ac.setCheckable(True) ac.setChecked(False) ac.toggled.connect(self.split_toggled) self.split_toggled(ac.isChecked()) self.bar.addAction(ac) ac = actions['reload-preview'] ac.triggered.connect(self.refresh) self.bar.addAction(ac) actions['preview-dock'].toggled.connect(self.visibility_changed) self.current_name = None self.last_sync_request = None self.refresh_timer = QTimer(self) self.refresh_timer.timeout.connect(self.refresh) parse_worker.start() self.current_sync_request = None self.search = HistoryLineEdit2(self) self.search.initialize('tweak_book_preview_search') self.search.setPlaceholderText(_('Search in preview')) self.search.returnPressed.connect(partial(self.find, 'next')) self.bar.addSeparator() self.bar.addWidget(self.search) for d in ('next', 'prev'): ac = actions['find-%s-preview' % d] ac.triggered.connect(partial(self.find, d)) self.bar.addAction(ac) def find(self, direction): text = unicode(self.search.text()) self.view.findText(text, QWebPage.FindWrapsAroundDocument | ( QWebPage.FindBackward if direction == 'prev' else QWebPage.FindFlags(0))) def request_sync(self, lnum): if self.current_name: self.sync_requested.emit(self.current_name, lnum) def request_split(self, loc): if self.current_name: self.split_requested.emit(self.current_name, loc) def sync_to_editor(self, name, lnum): self.current_sync_request = (name, lnum) QTimer.singleShot(100, self._sync_to_editor) def _sync_to_editor(self): if not actions['sync-preview-to-editor'].isChecked(): return try: if self.refresh_timer.isActive() or self.current_sync_request[0] != self.current_name: return QTimer.singleShot(100, self._sync_to_editor) except TypeError: return # Happens if current_sync_request is None lnum = self.current_sync_request[1] self.current_sync_request = None self.view.page().go_to_line(lnum) def show(self, name): if name != self.current_name: self.refresh_timer.stop() self.current_name = name parse_worker.add_request(name) self.view.setUrl(QUrl.fromLocalFile(current_container().name_to_abspath(name))) def refresh(self): if self.current_name: self.refresh_timer.stop() # This will check if the current html has changed in its editor, # and re-parse it if so parse_worker.add_request(self.current_name) # Tell webkit to reload all html and associated resources current_url = QUrl.fromLocalFile(current_container().name_to_abspath(self.current_name)) if current_url != self.view.url(): # The container was changed self.view.setUrl(current_url) else: self.view.refresh() def clear(self): self.view.clear() @property def is_visible(self): return actions['preview-dock'].isChecked() def start_refresh_timer(self): if self.is_visible and actions['auto-reload-preview'].isChecked(): self.refresh_timer.start(tprefs['preview_refresh_time'] * 1000) def stop_refresh_timer(self): self.refresh_timer.stop() def auto_reload_toggled(self, checked): actions['auto-reload-preview'].setToolTip(_( 'Auto reload preview when text changes in editor') if not checked else _( 'Disable auto reload of preview')) def sync_toggled(self, checked): actions['sync-preview-to-editor'].setToolTip(_( 'Disable syncing of preview position to editor position') if checked else _( 'Enable syncing of preview position to editor position')) def visibility_changed(self, is_visible): if is_visible: self.refresh() def split_toggled(self, checked): actions['split-in-preview'].setToolTip(textwrap.fill(_( 'Abort file split') if checked else _( 'Split this file at a specified location.\n\nAfter clicking this button, click' ' inside the preview panel above at the location you want the file to be split.'))) if checked: self.split_start_requested.emit() else: self.view.page().split_mode(False) def do_start_split(self): self.view.page().split_mode(True) def stop_split(self): actions['split-in-preview'].setChecked(False) def load_finished(self, ok): if actions['split-in-preview'].isChecked(): if ok: self.do_start_split() else: self.stop_split()
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.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))).toString()) result = json.loads(result) 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, name): self.start_update_timer() 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() 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 Ui_PartWindow(QWidget): """ The Ui_PartWindow class provides a Part Window UI object composed of three primary areas: - The "left area" contains the Project TabWidget which contains the Model Tree and Property Manager (tabs). Other tabs (widgets) can be added as needed. - The "right area" contains the Graphics Area (i.e. glpane) displaying the current model. - The "bottom area" lives below the left and right areas, spanning the full width of the part window. It can be used whenever a landscape layout is needed (i.e. the Sequence Editor). Typically, this area is not used and is hidden by default. A "part window" splitter lives between the left and right areas that allow the user to resize the shared area occupied by them. There is no splitter between the top and bottom areas. This class supports and is limited to a B{Single Document Interface (SDI)}. In time, NE1 will migrate to and support a Multiple Document Interface (MDI) to allow multiple project documents (i.e. parts, assemblies, simulations, text files, graphs, tables, etc. documents) to be available within the common workspace of the NE1 main window. @see: U{B{NE1 Main Window Framework} <http://www.nanoengineer-1.net/mediawiki/index.php?title=NE1_Main_Window_Framework>} """ widgets = [] # For debugging purposes. splitterPosition = PM_DEFAULT_WIDTH _previous_splitterPosition = PM_DEFAULT_WIDTH # Used for restoring the splitter position when collapsing/expanding # the left area. def __init__(self, assy, parent): """ Constructor for the part window. @param assy: The assembly (part) @type assy: Assembly @param parent: The parent widget. @type parent: U{B{QMainWindow} <http://doc.trolltech.com/4/qmainwindow.html>} """ QWidget.__init__(self, parent) self.parent = parent self.assy = assy # note: to support MDI, self.assy would probably need to be a # different assembly for each PartWindow. # [bruce 080216 comment] self.setWindowIcon(geticon("ui/border/Part.png")) self.updateWindowTitle() # The main layout for the part window is a VBoxLayout <pwVBoxLayout>. self.pwVBoxLayout = QVBoxLayout(self) pwVBoxLayout = self.pwVBoxLayout pwVBoxLayout.setMargin(0) pwVBoxLayout.setSpacing(0) # ################################################################ # <pwSplitter> is the horizontal splitter b/w the # pwLeftArea (mt and pm) and the glpane. self.pwSplitter = QSplitter(Qt.Horizontal) pwSplitter = self.pwSplitter pwSplitter.setObjectName("pwSplitter") pwSplitter.setHandleWidth(3) # 3 pixels wide. pwVBoxLayout.addWidget(pwSplitter) # ################################################################## # <pwLeftArea> is the container holding the pwProjectTabWidget. # Note: Making pwLeftArea (and pwRightArea and pwBottomArea) QFrame # widgets has the benefit of making it easy to draw a border around # each area. One purpose of this would be to help developers understand # (visually) how the part window is laid out. I intend to add a debug # pref to draw part window area borders and add "What's This" text to # them. Mark 2008-01-05. self.pwLeftArea = LeftFrame(self) pwLeftArea = self.pwLeftArea pwLeftArea.setObjectName("pwLeftArea") pwLeftArea.setMinimumWidth(PM_MINIMUM_WIDTH) pwLeftArea.setMaximumWidth(PM_MAXIMUM_WIDTH) # Setting the frame style like this is nice since it clearly # defines the splitter at the top-left corner. pwLeftArea.setFrameStyle(QFrame.Panel | QFrame.Sunken) # This layout will contain splitter (above) and the pwBottomArea. leftChannelVBoxLayout = QVBoxLayout(pwLeftArea) leftChannelVBoxLayout.setMargin(0) leftChannelVBoxLayout.setSpacing(0) pwSplitter.addWidget(pwLeftArea) # Makes it so pwLeftArea is not collapsible. pwSplitter.setCollapsible(0, False) # ################################################################## # <pwProjectTabWidget> is a QTabWidget that contains the MT and PM # widgets. It lives in the "left area" of the part window. self.pwProjectTabWidget = _pwProjectTabWidget() # _pwProjectTabWidget subclasses QTabWidget # Note [bruce 070829]: to fix bug 2522 I need to intercept # self.pwProjectTabWidget.removeTab, so I made it a subclass of # QTabWidget. It needs to know the GLPane, but that's not created # yet, so we set it later using KLUGE_setGLPane (below). # Note: No parent supplied. Could this be the source of the # minor vsplitter resizing problem I was trying to resolve a few # months ago? Try supplying a parent later. Mark 2008-01-01 self.pwProjectTabWidget.setObjectName("pwProjectTabWidget") self.pwProjectTabWidget.setCurrentIndex(0) self.pwProjectTabWidget.setAutoFillBackground(True) # Create the model tree "tab" widget. It will contain the MT GUI widget. # Set the tab icon, too. self.modelTreeTab = QWidget() self.modelTreeTab.setObjectName("modelTreeTab") self.pwProjectTabWidget.addTab(self.modelTreeTab, geticon("ui/modeltree/Model_Tree.png"), "") modelTreeTabLayout = QVBoxLayout(self.modelTreeTab) modelTreeTabLayout.setMargin(0) modelTreeTabLayout.setSpacing(0) # Create the model tree (GUI) and add it to the tab layout. self.modelTree = ModelTree(self.modelTreeTab, parent) self.modelTree.modelTreeGui.setObjectName("modelTreeGui") modelTreeTabLayout.addWidget(self.modelTree.modelTreeGui) # Create the property manager "tab" widget. It will contain the PropMgr # scroll area, which will contain the property manager and all its # widgets. self.propertyManagerTab = QWidget() self.propertyManagerTab.setObjectName("propertyManagerTab") self.propertyManagerScrollArea = QScrollArea(self.pwProjectTabWidget) self.propertyManagerScrollArea.setObjectName( "propertyManagerScrollArea") self.propertyManagerScrollArea.setWidget(self.propertyManagerTab) self.propertyManagerScrollArea.setWidgetResizable(True) # Eureka! # setWidgetResizable(True) will resize the Property Manager (and its # contents) correctly when the scrollbar appears/disappears. # It even accounts correctly for collapsed/expanded groupboxes! # Mark 2007-05-29 # Add the property manager scroll area as a "tabbed" widget. # Set the tab icon, too. self.pwProjectTabWidget.addTab( self.propertyManagerScrollArea, geticon("ui/modeltree/Property_Manager.png"), "") # Finally, add the "pwProjectTabWidget" to the left channel layout. leftChannelVBoxLayout.addWidget(self.pwProjectTabWidget) # Create the glpane and make it a child of the part splitter. self.glpane = GLPane(assy, self, 'glpane name', parent) # note: our owner (MWsemantics) assumes # there is just this one GLPane for assy, and stores it # into assy as assy.o and assy.glpane. [bruce 080216 comment] # Add what's this text to self.glpane. # [bruce 080912 moved this here from part of a method in class GLPane. # In this code's old location, Mark wrote [2007-06-01]: "Problem - # I don't believe this text is processed by fix_whatsthis_text_and_links() # in whatsthis_utilities.py." Now that this code is here, I don't know # whether that's still true. ] from ne1_ui.WhatsThisText_for_MainWindow import whats_this_text_for_glpane self.glpane.setWhatsThis(whats_this_text_for_glpane()) # update [re the above comment], bruce 081209: # I added the following explicit call of fix_whatsthis_text_and_links, # but it doesn't work to replace Ctrl with Cmd on Mac; # see today's comment in fix_whatsthis_text_and_links for likely reason. # So I will leave this here, but also leave in place the kluges # in whats_this_text_for_glpane to do that replacement itself. # The wiki help link in this whatsthis text doesn't work, # but I guess that is an independent issue, related to lack # of use of class QToolBar_WikiHelp or similar code, for GLPane # or this class or the main window class. from foundation.whatsthis_utilities import fix_whatsthis_text_and_links fix_whatsthis_text_and_links(self.glpane) # doesn't yet work self.pwProjectTabWidget.KLUGE_setGLPane(self.glpane) # help fix bug 2522 [bruce 070829] qt4warnDestruction(self.glpane, 'GLPane of PartWindow') pwSplitter.addWidget(self.glpane) # ################################################################## # <pwBottomArea> is a container at the bottom of the part window # spanning its entire width. It is intended to be used as an extra # area for use by Property Managers (or anything else) that needs # a landscape oriented layout. # An example is the Sequence Editor, which is part of the # Strand Properties PM. self.pwBottomArea = QFrame() # IMHO, self is not a good parent. Mark 2008-01-04. pwBottomArea = self.pwBottomArea pwBottomArea.setObjectName("pwBottomArea") pwBottomArea.setMaximumHeight(50) # Add a frame border to see what it looks like. pwBottomArea.setFrameStyle(QFrame.Panel | QFrame.Sunken) self.pwVBoxLayout.addWidget(pwBottomArea) # Hide the bottom frame for now. Later this might be used for the # sequence editor. pwBottomArea.hide() #This widget implementation is subject to heavy revision. The purpose #is to implement a NFR that Mark urgently needs : The NFR is: Need a #way to quickly find a node in the MT by entering its name. #-- Ninad 2008-11-06 self.pwSpecialDockWidgetInLeftChannel = SelectNodeByNameDockWidget( self.glpane.win) leftChannelVBoxLayout.addWidget(self.pwSpecialDockWidgetInLeftChannel) # See the resizeEvent() docstring for more information about # resizeTimer. self.resizeTimer = QTimer(self) self.resizeTimer.setSingleShot(True) return def getLeftChannelDockWidget(self): return self.pwSpecialDockWidgetInLeftChannel def updateWindowTitle(self, changed=False): #by mark; bruce 050810 revised this in several ways, fixed bug 785 """ Update the window title (caption) at the top of the part window. Example: "partname.mmp" This implements the standard way most applications indicate that a document has unsaved changes. On Mac OS X the close button will have a modified look; on other platforms the window title will have an '*' (asterisk). @note: We'll want to experiment with this to make sure it @param changed: If True, the document has unsaved changes. @type changed: boolean @see: U{B{windowTitle}<http://doc.trolltech.com/4/qwidget.html#windowTitle-prop>}, U{B{windowModified}<http://doc.trolltech.com/4/qwidget.html#windowModified-prop>} """ # WARNING: there is mostly-duplicated code in this method and in # MWsemantics.update_mainwindow_caption. See the comment in that # method for more info and todos. [bruce 081227 comment] caption_fullpath = env.prefs[captionFullPath_prefs_key] partname = "Untitled" # fallback value if no file yet if self.assy.filename: #bruce 081227 cleanup: try -> if, etc # self.assy.filename is always an empty string, even after a # file has been opened with a complete name. Need to ask Bruce # about this problem, resulting in a bug (i.e. the window title # is always "Untitled". Mark 2008-01-02. junk, basename = os.path.split(self.assy.filename) if basename: if caption_fullpath: partname = os.path.normpath(self.assy.filename) #fixed bug 453-1 ninad060721 else: partname = basename # WARNING: the following code differs in the two versions # of this routine. # The "[*]" placeholder below is modified or removed by Qt; see: # http://doc.trolltech.com/4/qwidget.html#windowModified-prop self.setWindowTitle(self.trUtf8(partname + '[*]')) self.setWindowModified(changed) # replaces '[*]' by '' or '*' return def collapseLeftArea(self, hideLeftArea=True): """ Make the left area collapsible (via the splitter). The left area will be hidden (collapsed,actually) if I{hideLeftArea} is True (the default). """ self._previous_splitterPosition = self.pwLeftArea.width() if hideLeftArea: self.pwSplitter.setCollapsible(0, True) self.setSplitterPosition(pos=0) return def expandLeftArea(self): """ Expand the left area. @see: L{MWsemantics._showFullScreenCommonCode()} for an example showing how it is used. """ self.setSplitterPosition(pos=self._previous_splitterPosition) self.pwSplitter.setCollapsible(0, False) self.pwLeftArea.setMinimumWidth(PM_MINIMUM_WIDTH) self.pwLeftArea.setMaximumWidth(PM_MAXIMUM_WIDTH) return def updatePropertyManagerTab(self, tab): #Ninad 061207 "Update the Properties Manager tab with 'tab' " self.parent.glpane.gl_update_confcorner() #bruce 070627, since PM affects confcorner appearance if self.propertyManagerScrollArea.widget(): # The following is necessary to get rid of those C object # deleted errors (and the resulting bugs) lastwidgetobject = self.propertyManagerScrollArea.takeWidget() if lastwidgetobject: # bruce 071018 revised this code; see my comment on same # code in PM_Dialog try: lastwidgetobject.update_props_if_needed_before_closing except AttributeError: if 1 or debug_flags.atom_debug: msg1 = "Last PropMgr %r doesn't have method" % lastwidgetobject msg2 = " update_props_if_needed_before_closing. That's" msg3 = " OK (for now, only implemented for Plane PM). " msg4 = "Ignoring Exception: " print_compact_traceback(msg1 + msg2 + msg3 + msg4) else: lastwidgetobject.update_props_if_needed_before_closing() lastwidgetobject.hide() # @ ninad 061212 perhaps hiding the widget is not needed self.pwProjectTabWidget.removeTab( self.pwProjectTabWidget.indexOf(self.propertyManagerScrollArea)) # Set the PropertyManager tab scroll area to the appropriate widget. self.propertyManagerScrollArea.setWidget(tab) self.pwProjectTabWidget.addTab( self.propertyManagerScrollArea, geticon("ui/modeltree/Property_Manager.png"), "") self.pwProjectTabWidget.setCurrentIndex( self.pwProjectTabWidget.indexOf(self.propertyManagerScrollArea)) return def KLUGE_current_PropertyManager(self): #bruce 070627; revised 070829 as part of fixing bug 2523 """ Return the current Property Manager widget (whether or not its tab is chosen, but only if it has a tab), or None if there is not one. @warning: This method's existence (not only its implementation) is a kluge, since the right way to access that would be by asking the "command sequencer"; but that's not yet implemented, so this is the best we can do for now. Also, it would be better to get the top command and talk to it, not its PM (a QWidget). Also, whatever calls this will be making assumptions about that PM which are really only the command's business. So in short, every call of this is in need of cleanup once we have a working "command sequencer". (That's true of many things related to PMs, not only this method.) @warning: The return values are (presumably) widgets, but they can also be mode objects and generator objects, due to excessive use of multiple inheritance in the current PM code. So be careful what you do with them -- they might have lots of extra methods/attrs, and setting your own attrs in them might mess things up. """ res = self.propertyManagerScrollArea.widget() if not hasattr(res, 'done_btn'): # not sure what widget this is otherwise, but it is a widget # (not None) for the default mode, at least on startup, so just # return None in this case return None # Sometimes this PM remains present from a prior command, even when # there is no longer a tab for the PM. As part of fixing bug 2523 # we have to avoid returning it in that case. How we do that is a kluge, # but hopefully this entire kluge function can be dispensed with soon. # This change also fixes bug 2522 on the Mac (but not on Windows -- # for that, we needed to intercept removeTab in separate code above). index = self.pwProjectTabWidget.indexOf(self.propertyManagerScrollArea) if index == -1: return None # Due to bugs in other code, sometimes the PM tab is left in place, # though the PM itself is hidden. To avoid finding the PM in that case, # also check whether it's hidden. This will fix the CC part of a new bug # just reported by Keith in email (when hitting Ok in DNA Gen). if res.isHidden(): # probably a QWidget method [bruce 080205 comment] return None return res def dismiss(self): self.parent.removePartWindow(self) return def setSplitterPosition(self, pos=PM_DEFAULT_WIDTH, setDefault=True): """ Set the position of the splitter between the left area and graphics area so that the width of the container holding the model tree (and property manager) is I{pos} pixels wide. @param pos: The splitter position (in pixel units). @type pos: int @param setDefault: If True (the default), I{pos} becomes the new default position. @type setDefault: boolean """ self.pwSplitter.moveSplitter(pos, 1) if _DEBUG: print "New Splitter Position: %d (setDefault=%d)" \ % (pos, setDefault) if setDefault: self.splitterPosition = pos return def resizeEvent(self, event): """ This reimplementation of QWidget.resizeEvent is here to deal with the undesired behavior of the splitter while resizing the part window. Normally, the splitter will drift back and forth while resizing the part window. This forces the splitter to stay fixed during resize operations. """ # When self.resizeTimer.isActive() = True, the partwindow is being # resized. This is checked by the resizeEvent handler in LeftFrame # to determine if the splitter is being moved by the user or # programmably by self's resizeEvent. if self.resizeTimer.isActive(): self.resizeTimer.stop() # Stop the timer. self.resizeTimer.start(500) # (Re)strand a .5 second singleshot timer. self.setSplitterPosition(self.splitterPosition, setDefault=False) QWidget.resizeEvent(self, event) return
class Ui_PartWindow(QWidget): """ The Ui_PartWindow class provides a Part Window UI object composed of three primary areas: - The "left area" contains the Project TabWidget which contains the Model Tree and Property Manager (tabs). Other tabs (widgets) can be added as needed. - The "right area" contains the Graphics Area (i.e. glpane) displaying the current model. - The "bottom area" lives below the left and right areas, spanning the full width of the part window. It can be used whenever a landscape layout is needed (i.e. the Sequence Editor). Typically, this area is not used and is hidden by default. A "part window" splitter lives between the left and right areas that allow the user to resize the shared area occupied by them. There is no splitter between the top and bottom areas. This class supports and is limited to a B{Single Document Interface (SDI)}. In time, NE1 will migrate to and support a Multiple Document Interface (MDI) to allow multiple project documents (i.e. parts, assemblies, simulations, text files, graphs, tables, etc. documents) to be available within the common workspace of the NE1 main window. @see: U{B{NE1 Main Window Framework} <http://www.nanoengineer-1.net/mediawiki/index.php?title=NE1_Main_Window_Framework>} """ widgets = [] # For debugging purposes. splitterPosition = PM_DEFAULT_WIDTH _previous_splitterPosition = PM_DEFAULT_WIDTH # Used for restoring the splitter position when collapsing/expanding # the left area. def __init__(self, assy, parent): """ Constructor for the part window. @param assy: The assembly (part) @type assy: Assembly @param parent: The parent widget. @type parent: U{B{QMainWindow} <http://doc.trolltech.com/4/qmainwindow.html>} """ QWidget.__init__(self, parent) self.parent = parent self.assy = assy # note: to support MDI, self.assy would probably need to be a # different assembly for each PartWindow. # [bruce 080216 comment] self.setWindowIcon(geticon("ui/border/Part.png")) self.updateWindowTitle() # The main layout for the part window is a VBoxLayout <pwVBoxLayout>. self.pwVBoxLayout = QVBoxLayout(self) pwVBoxLayout = self.pwVBoxLayout pwVBoxLayout.setMargin(0) pwVBoxLayout.setSpacing(0) # ################################################################ # <pwSplitter> is the horizontal splitter b/w the # pwLeftArea (mt and pm) and the glpane. self.pwSplitter = QSplitter(Qt.Horizontal) pwSplitter = self.pwSplitter pwSplitter.setObjectName("pwSplitter") pwSplitter.setHandleWidth(3) # 3 pixels wide. pwVBoxLayout.addWidget(pwSplitter) # ################################################################## # <pwLeftArea> is the container holding the pwProjectTabWidget. # Note: Making pwLeftArea (and pwRightArea and pwBottomArea) QFrame # widgets has the benefit of making it easy to draw a border around # each area. One purpose of this would be to help developers understand # (visually) how the part window is laid out. I intend to add a debug # pref to draw part window area borders and add "What's This" text to # them. Mark 2008-01-05. self.pwLeftArea = LeftFrame(self) pwLeftArea = self.pwLeftArea pwLeftArea.setObjectName("pwLeftArea") pwLeftArea.setMinimumWidth(PM_MINIMUM_WIDTH) pwLeftArea.setMaximumWidth(PM_MAXIMUM_WIDTH) # Setting the frame style like this is nice since it clearly # defines the splitter at the top-left corner. pwLeftArea.setFrameStyle( QFrame.Panel | QFrame.Sunken ) # This layout will contain splitter (above) and the pwBottomArea. leftChannelVBoxLayout = QVBoxLayout(pwLeftArea) leftChannelVBoxLayout.setMargin(0) leftChannelVBoxLayout.setSpacing(0) pwSplitter.addWidget(pwLeftArea) # Makes it so pwLeftArea is not collapsible. pwSplitter.setCollapsible (0, False) # ################################################################## # <pwProjectTabWidget> is a QTabWidget that contains the MT and PM # widgets. It lives in the "left area" of the part window. self.pwProjectTabWidget = _pwProjectTabWidget() # _pwProjectTabWidget subclasses QTabWidget # Note [bruce 070829]: to fix bug 2522 I need to intercept # self.pwProjectTabWidget.removeTab, so I made it a subclass of # QTabWidget. It needs to know the GLPane, but that's not created # yet, so we set it later using KLUGE_setGLPane (below). # Note: No parent supplied. Could this be the source of the # minor vsplitter resizing problem I was trying to resolve a few # months ago? Try supplying a parent later. Mark 2008-01-01 self.pwProjectTabWidget.setObjectName("pwProjectTabWidget") self.pwProjectTabWidget.setCurrentIndex(0) self.pwProjectTabWidget.setAutoFillBackground(True) # Create the model tree "tab" widget. It will contain the MT GUI widget. # Set the tab icon, too. self.modelTreeTab = QWidget() self.modelTreeTab.setObjectName("modelTreeTab") self.pwProjectTabWidget.addTab( self.modelTreeTab, geticon("ui/modeltree/Model_Tree.png"), "") modelTreeTabLayout = QVBoxLayout(self.modelTreeTab) modelTreeTabLayout.setMargin(0) modelTreeTabLayout.setSpacing(0) # Create the model tree (GUI) and add it to the tab layout. self.modelTree = ModelTree(self.modelTreeTab, parent) self.modelTree.modelTreeGui.setObjectName("modelTreeGui") modelTreeTabLayout.addWidget(self.modelTree.modelTreeGui) # Create the property manager "tab" widget. It will contain the PropMgr # scroll area, which will contain the property manager and all its # widgets. self.propertyManagerTab = QWidget() self.propertyManagerTab.setObjectName("propertyManagerTab") self.propertyManagerScrollArea = QScrollArea(self.pwProjectTabWidget) self.propertyManagerScrollArea.setObjectName("propertyManagerScrollArea") self.propertyManagerScrollArea.setWidget(self.propertyManagerTab) self.propertyManagerScrollArea.setWidgetResizable(True) # Eureka! # setWidgetResizable(True) will resize the Property Manager (and its # contents) correctly when the scrollbar appears/disappears. # It even accounts correctly for collapsed/expanded groupboxes! # Mark 2007-05-29 # Add the property manager scroll area as a "tabbed" widget. # Set the tab icon, too. self.pwProjectTabWidget.addTab( self.propertyManagerScrollArea, geticon("ui/modeltree/Property_Manager.png"), "") # Finally, add the "pwProjectTabWidget" to the left channel layout. leftChannelVBoxLayout.addWidget(self.pwProjectTabWidget) # Create the glpane and make it a child of the part splitter. self.glpane = GLPane(assy, self, 'glpane name', parent) # note: our owner (MWsemantics) assumes # there is just this one GLPane for assy, and stores it # into assy as assy.o and assy.glpane. [bruce 080216 comment] # Add what's this text to self.glpane. # [bruce 080912 moved this here from part of a method in class GLPane. # In this code's old location, Mark wrote [2007-06-01]: "Problem - # I don't believe this text is processed by fix_whatsthis_text_and_links() # in whatsthis_utilities.py." Now that this code is here, I don't know # whether that's still true. ] from ne1_ui.WhatsThisText_for_MainWindow import whats_this_text_for_glpane self.glpane.setWhatsThis( whats_this_text_for_glpane() ) # update [re the above comment], bruce 081209: # I added the following explicit call of fix_whatsthis_text_and_links, # but it doesn't work to replace Ctrl with Cmd on Mac; # see today's comment in fix_whatsthis_text_and_links for likely reason. # So I will leave this here, but also leave in place the kluges # in whats_this_text_for_glpane to do that replacement itself. # The wiki help link in this whatsthis text doesn't work, # but I guess that is an independent issue, related to lack # of use of class QToolBar_WikiHelp or similar code, for GLPane # or this class or the main window class. from foundation.whatsthis_utilities import fix_whatsthis_text_and_links fix_whatsthis_text_and_links(self.glpane) # doesn't yet work self.pwProjectTabWidget.KLUGE_setGLPane(self.glpane) # help fix bug 2522 [bruce 070829] qt4warnDestruction(self.glpane, 'GLPane of PartWindow') pwSplitter.addWidget(self.glpane) # ################################################################## # <pwBottomArea> is a container at the bottom of the part window # spanning its entire width. It is intended to be used as an extra # area for use by Property Managers (or anything else) that needs # a landscape oriented layout. # An example is the Sequence Editor, which is part of the # Strand Properties PM. self.pwBottomArea = QFrame() # IMHO, self is not a good parent. Mark 2008-01-04. pwBottomArea = self.pwBottomArea pwBottomArea.setObjectName("pwBottomArea") pwBottomArea.setMaximumHeight(50) # Add a frame border to see what it looks like. pwBottomArea.setFrameStyle( QFrame.Panel | QFrame.Sunken ) self.pwVBoxLayout.addWidget(pwBottomArea) # Hide the bottom frame for now. Later this might be used for the # sequence editor. pwBottomArea.hide() #This widget implementation is subject to heavy revision. The purpose #is to implement a NFR that Mark urgently needs : The NFR is: Need a #way to quickly find a node in the MT by entering its name. #-- Ninad 2008-11-06 self.pwSpecialDockWidgetInLeftChannel = SelectNodeByNameDockWidget(self.glpane.win) leftChannelVBoxLayout.addWidget(self.pwSpecialDockWidgetInLeftChannel) # See the resizeEvent() docstring for more information about # resizeTimer. self.resizeTimer = QTimer(self) self.resizeTimer.setSingleShot(True) return def getLeftChannelDockWidget(self): return self.pwSpecialDockWidgetInLeftChannel def updateWindowTitle(self, changed = False): #by mark; bruce 050810 revised this in several ways, fixed bug 785 """ Update the window title (caption) at the top of the part window. Example: "partname.mmp" This implements the standard way most applications indicate that a document has unsaved changes. On Mac OS X the close button will have a modified look; on other platforms the window title will have an '*' (asterisk). @note: We'll want to experiment with this to make sure it @param changed: If True, the document has unsaved changes. @type changed: boolean @see: U{B{windowTitle}<http://doc.trolltech.com/4/qwidget.html#windowTitle-prop>}, U{B{windowModified}<http://doc.trolltech.com/4/qwidget.html#windowModified-prop>} """ # WARNING: there is mostly-duplicated code in this method and in # MWsemantics.update_mainwindow_caption. See the comment in that # method for more info and todos. [bruce 081227 comment] caption_fullpath = env.prefs[captionFullPath_prefs_key] partname = "Untitled" # fallback value if no file yet if self.assy.filename: #bruce 081227 cleanup: try -> if, etc # self.assy.filename is always an empty string, even after a # file has been opened with a complete name. Need to ask Bruce # about this problem, resulting in a bug (i.e. the window title # is always "Untitled". Mark 2008-01-02. junk, basename = os.path.split(self.assy.filename) if basename: if caption_fullpath: partname = os.path.normpath(self.assy.filename) #fixed bug 453-1 ninad060721 else: partname = basename # WARNING: the following code differs in the two versions # of this routine. # The "[*]" placeholder below is modified or removed by Qt; see: # http://doc.trolltech.com/4/qwidget.html#windowModified-prop self.setWindowTitle(self.trUtf8(partname + '[*]')) self.setWindowModified(changed) # replaces '[*]' by '' or '*' return def collapseLeftArea(self, hideLeftArea = True): """ Make the left area collapsible (via the splitter). The left area will be hidden (collapsed,actually) if I{hideLeftArea} is True (the default). """ self._previous_splitterPosition = self.pwLeftArea.width() if hideLeftArea: self.pwSplitter.setCollapsible(0, True) self.setSplitterPosition(pos = 0) return def expandLeftArea(self): """ Expand the left area. @see: L{MWsemantics._showFullScreenCommonCode()} for an example showing how it is used. """ self.setSplitterPosition(pos = self._previous_splitterPosition) self.pwSplitter.setCollapsible(0, False) self.pwLeftArea.setMinimumWidth(PM_MINIMUM_WIDTH) self.pwLeftArea.setMaximumWidth(PM_MAXIMUM_WIDTH) return def updatePropertyManagerTab(self, tab): #Ninad 061207 "Update the Properties Manager tab with 'tab' " self.parent.glpane.gl_update_confcorner() #bruce 070627, since PM affects confcorner appearance if self.propertyManagerScrollArea.widget(): # The following is necessary to get rid of those C object # deleted errors (and the resulting bugs) lastwidgetobject = self.propertyManagerScrollArea.takeWidget() if lastwidgetobject: # bruce 071018 revised this code; see my comment on same # code in PM_Dialog try: lastwidgetobject.update_props_if_needed_before_closing except AttributeError: if 1 or debug_flags.atom_debug: msg1 = "Last PropMgr %r doesn't have method" % lastwidgetobject msg2 =" update_props_if_needed_before_closing. That's" msg3 = " OK (for now, only implemented for Plane PM). " msg4 = "Ignoring Exception: " print_compact_traceback(msg1 + msg2 + msg3 + msg4) else: lastwidgetobject.update_props_if_needed_before_closing() lastwidgetobject.hide() # @ ninad 061212 perhaps hiding the widget is not needed self.pwProjectTabWidget.removeTab( self.pwProjectTabWidget.indexOf(self.propertyManagerScrollArea)) # Set the PropertyManager tab scroll area to the appropriate widget. self.propertyManagerScrollArea.setWidget(tab) self.pwProjectTabWidget.addTab( self.propertyManagerScrollArea, geticon("ui/modeltree/Property_Manager.png"), "") self.pwProjectTabWidget.setCurrentIndex( self.pwProjectTabWidget.indexOf(self.propertyManagerScrollArea)) return def KLUGE_current_PropertyManager(self): #bruce 070627; revised 070829 as part of fixing bug 2523 """ Return the current Property Manager widget (whether or not its tab is chosen, but only if it has a tab), or None if there is not one. @warning: This method's existence (not only its implementation) is a kluge, since the right way to access that would be by asking the "command sequencer"; but that's not yet implemented, so this is the best we can do for now. Also, it would be better to get the top command and talk to it, not its PM (a QWidget). Also, whatever calls this will be making assumptions about that PM which are really only the command's business. So in short, every call of this is in need of cleanup once we have a working "command sequencer". (That's true of many things related to PMs, not only this method.) @warning: The return values are (presumably) widgets, but they can also be mode objects and generator objects, due to excessive use of multiple inheritance in the current PM code. So be careful what you do with them -- they might have lots of extra methods/attrs, and setting your own attrs in them might mess things up. """ res = self.propertyManagerScrollArea.widget() if not hasattr(res, 'done_btn'): # not sure what widget this is otherwise, but it is a widget # (not None) for the default mode, at least on startup, so just # return None in this case return None # Sometimes this PM remains present from a prior command, even when # there is no longer a tab for the PM. As part of fixing bug 2523 # we have to avoid returning it in that case. How we do that is a kluge, # but hopefully this entire kluge function can be dispensed with soon. # This change also fixes bug 2522 on the Mac (but not on Windows -- # for that, we needed to intercept removeTab in separate code above). index = self.pwProjectTabWidget.indexOf( self.propertyManagerScrollArea) if index == -1: return None # Due to bugs in other code, sometimes the PM tab is left in place, # though the PM itself is hidden. To avoid finding the PM in that case, # also check whether it's hidden. This will fix the CC part of a new bug # just reported by Keith in email (when hitting Ok in DNA Gen). if res.isHidden(): # probably a QWidget method [bruce 080205 comment] return None return res def dismiss(self): self.parent.removePartWindow(self) return def setSplitterPosition(self, pos = PM_DEFAULT_WIDTH, setDefault = True): """ Set the position of the splitter between the left area and graphics area so that the width of the container holding the model tree (and property manager) is I{pos} pixels wide. @param pos: The splitter position (in pixel units). @type pos: int @param setDefault: If True (the default), I{pos} becomes the new default position. @type setDefault: boolean """ self.pwSplitter.moveSplitter(pos, 1) if _DEBUG: print "New Splitter Position: %d (setDefault=%d)" \ % (pos, setDefault) if setDefault: self.splitterPosition = pos return def resizeEvent(self, event): """ This reimplementation of QWidget.resizeEvent is here to deal with the undesired behavior of the splitter while resizing the part window. Normally, the splitter will drift back and forth while resizing the part window. This forces the splitter to stay fixed during resize operations. """ # When self.resizeTimer.isActive() = True, the partwindow is being # resized. This is checked by the resizeEvent handler in LeftFrame # to determine if the splitter is being moved by the user or # programmably by self's resizeEvent. if self.resizeTimer.isActive(): self.resizeTimer.stop() # Stop the timer. self.resizeTimer.start( 500 ) # (Re)strand a .5 second singleshot timer. self.setSplitterPosition(self.splitterPosition, setDefault = False) QWidget.resizeEvent(self, event) return
class Preview(QWidget): sync_requested = pyqtSignal(object, object) split_requested = pyqtSignal(object, object, object) split_start_requested = pyqtSignal() link_clicked = pyqtSignal(object, object) refresh_starting = pyqtSignal() refreshed = pyqtSignal() def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QVBoxLayout() self.setLayout(l) l.setContentsMargins(0, 0, 0, 0) self.view = WebView(self) self.view.page().sync_requested.connect(self.request_sync) self.view.page().split_requested.connect(self.request_split) self.view.page().loadFinished.connect(self.load_finished) self.inspector = self.view.inspector self.inspector.setPage(self.view.page()) l.addWidget(self.view) self.bar = QToolBar(self) l.addWidget(self.bar) ac = actions['auto-reload-preview'] ac.setCheckable(True) ac.setChecked(True) ac.toggled.connect(self.auto_reload_toggled) self.auto_reload_toggled(ac.isChecked()) self.bar.addAction(ac) ac = actions['sync-preview-to-editor'] ac.setCheckable(True) ac.setChecked(True) ac.toggled.connect(self.sync_toggled) self.sync_toggled(ac.isChecked()) self.bar.addAction(ac) self.bar.addSeparator() ac = actions['split-in-preview'] ac.setCheckable(True) ac.setChecked(False) ac.toggled.connect(self.split_toggled) self.split_toggled(ac.isChecked()) self.bar.addAction(ac) ac = actions['reload-preview'] ac.triggered.connect(self.refresh) self.bar.addAction(ac) actions['preview-dock'].toggled.connect(self.visibility_changed) self.current_name = None self.last_sync_request = None self.refresh_timer = QTimer(self) self.refresh_timer.timeout.connect(self.refresh) parse_worker.start() self.current_sync_request = None self.search = HistoryLineEdit2(self) self.search.initialize('tweak_book_preview_search') self.search.setPlaceholderText(_('Search in preview')) self.search.returnPressed.connect(partial(self.find, 'next')) self.bar.addSeparator() self.bar.addWidget(self.search) for d in ('next', 'prev'): ac = actions['find-%s-preview' % d] ac.triggered.connect(partial(self.find, d)) self.bar.addAction(ac) def find(self, direction): text = unicode(self.search.text()) self.view.findText( text, QWebPage.FindWrapsAroundDocument | (QWebPage.FindBackward if direction == 'prev' else QWebPage.FindFlags(0))) def request_sync(self, tagname, href, lnum): if self.current_name: c = current_container() if tagname == 'a' and href: if href and href.startswith('#'): name = self.current_name else: name = c.href_to_name(href, self.current_name) if href else None if name == self.current_name: return self.view.page().go_to_anchor( urlparse(href).fragment, lnum) if name and c.exists(name) and c.mime_map[name] in OEB_DOCS: return self.link_clicked.emit( name, urlparse(href).fragment or TOP) self.sync_requested.emit(self.current_name, lnum) def request_split(self, loc, totals): if self.current_name: self.split_requested.emit(self.current_name, loc, totals) def sync_to_editor(self, name, sourceline_address): self.current_sync_request = (name, sourceline_address) QTimer.singleShot(100, self._sync_to_editor) def _sync_to_editor(self): if not actions['sync-preview-to-editor'].isChecked(): return try: if self.refresh_timer.isActive( ) or self.current_sync_request[0] != self.current_name: return QTimer.singleShot(100, self._sync_to_editor) except TypeError: return # Happens if current_sync_request is None sourceline_address = self.current_sync_request[1] self.current_sync_request = None self.view.page().go_to_sourceline_address(sourceline_address) def show(self, name): if name != self.current_name: self.refresh_timer.stop() self.current_name = name parse_worker.add_request(name) self.view.setUrl( QUrl.fromLocalFile(current_container().name_to_abspath(name))) return True def refresh(self): if self.current_name: self.refresh_timer.stop() # This will check if the current html has changed in its editor, # and re-parse it if so parse_worker.add_request(self.current_name) # Tell webkit to reload all html and associated resources current_url = QUrl.fromLocalFile( current_container().name_to_abspath(self.current_name)) self.refresh_starting.emit() if current_url != self.view.url(): # The container was changed self.view.setUrl(current_url) else: self.view.refresh() self.refreshed.emit() def clear(self): self.view.clear() self.current_name = None @property def current_root(self): return self.view.page().current_root @property def is_visible(self): return actions['preview-dock'].isChecked() @property def live_css_is_visible(self): try: return actions['live-css-dock'].isChecked() except KeyError: return False def start_refresh_timer(self): if self.live_css_is_visible or ( self.is_visible and actions['auto-reload-preview'].isChecked()): self.refresh_timer.start(tprefs['preview_refresh_time'] * 1000) def stop_refresh_timer(self): self.refresh_timer.stop() def auto_reload_toggled(self, checked): if self.live_css_is_visible and not actions[ 'auto-reload-preview'].isChecked(): actions['auto-reload-preview'].setChecked(True) error_dialog( self, _('Cannot disable'), _('Auto reloading of the preview panel cannot be disabled while the' ' Live CSS panel is open.'), show=True) actions['auto-reload-preview'].setToolTip( _('Auto reload preview when text changes in editor' ) if not checked else _('Disable auto reload of preview')) def sync_toggled(self, checked): actions['sync-preview-to-editor'].setToolTip( _('Disable syncing of preview position to editor position' ) if checked else _( 'Enable syncing of preview position to editor position')) def visibility_changed(self, is_visible): if is_visible: self.refresh() def split_toggled(self, checked): actions['split-in-preview'].setToolTip( textwrap.fill( _('Abort file split') if checked else _('Split this file at a specified location.\n\nAfter clicking this button, click' ' inside the preview panel above at the location you want the file to be split.' ))) if checked: self.split_start_requested.emit() else: self.view.page().split_mode(False) def do_start_split(self): self.view.page().split_mode(True) def stop_split(self): actions['split-in-preview'].setChecked(False) def load_finished(self, ok): if actions['split-in-preview'].isChecked(): if ok: self.do_start_split() else: self.stop_split() def apply_settings(self): s = self.view.page().settings() s.setFontSize(s.DefaultFontSize, tprefs['preview_base_font_size']) s.setFontSize(s.DefaultFixedFontSize, tprefs['preview_mono_font_size']) s.setFontSize(s.MinimumLogicalFontSize, tprefs['preview_minimum_font_size']) s.setFontSize(s.MinimumFontSize, tprefs['preview_minimum_font_size']) sf, ssf, mf = tprefs['preview_serif_family'], tprefs[ 'preview_sans_family'], tprefs['preview_mono_family'] s.setFontFamily(s.StandardFont, { 'serif': sf, 'sans': ssf, 'mono': mf, None: sf }[tprefs['preview_standard_font_family']]) s.setFontFamily(s.SerifFont, sf) s.setFontFamily(s.SansSerifFont, ssf) s.setFontFamily(s.FixedFont, mf)
class Widget(QWidget, ScreenWidget): name = "timeSetup" def __init__(self): QWidget.__init__(self) self.ui = Ui_DateTimeWidget() self.ui.setupUi(self) self.timer = QTimer(self) self.from_time_updater = True self.is_date_changed = False self.current_zone = "" self.tz_dict = {} self.continents = [] self.countries = [] for country, data in yali.localedata.locales.items(): if country == ctx.consts.lang: if data.has_key("timezone"): ctx.installData.timezone = data["timezone"] # Append continents and countries the time zone dictionary self.createTZDictionary() # Sort continent list self.sortContinents() # Append sorted continents to combobox self.loadContinents() # Load current continents country list self.getCountries(self.current_zone["continent"]) # Highlight the current zone self.index = self.ui.continentList.findText(self.current_zone["continent"]) self.ui.continentList.setCurrentIndex(self.index) self.index = self.ui.countryList.findText(self.current_zone["country"]) self.ui.countryList.setCurrentIndex(self.index) # Initialize widget signal and slots self.__initSignals__() self.ui.calendarWidget.setDate(QDate.currentDate()) self.pthread = None self.pds_messagebox = PMessageBox(self) self.pds_messagebox.enableOverlay() self.timer.start(1000) def __initSignals__(self): self.connect(self.ui.timeEdit, SIGNAL("timeChanged(QTime)"), self.timerStop) self.connect(self.ui.calendarWidget, SIGNAL("selectionChanged()"), self.dateChanged) self.connect(self.timer, SIGNAL("timeout()"), self.updateClock) self.connect(self.ui.continentList, SIGNAL("activated(QString)"), self.getCountries) def createTZDictionary(self): tz = TimeZoneList() zones = [ x.timeZone for x in tz.getEntries() ] zones.sort() for zone in zones: split = zone.split("/") # Human readable continent names continent_pretty_name = split[0].replace("_", " ") continent_pretty_name = continent_pretty_name # Some country names can be like Argentina/Catamarca so this fixes the splitting problem # caused by zone.split("/") # # Remove continent info and take the rest as the country name split.pop(0) country_pretty_name = " / ".join(split) # Human readable country names country_pretty_name = country_pretty_name.replace("_", " ") # Get current zone if zone == ctx.installData.timezone: self.current_zone = { "continent":continent_pretty_name, "country":country_pretty_name} # Append to dictionary if self.tz_dict.has_key(continent_pretty_name): self.tz_dict[continent_pretty_name].append([country_pretty_name, zone]) else: self.tz_dict[continent_pretty_name] = [[country_pretty_name, zone]] def sortContinents(self): for continent in self.tz_dict.keys(): self.continents.append(continent) self.continents.sort() def loadContinents(self): for continent in self.continents: self.ui.continentList.addItem(continent) def getCountries(self, continent): # Countries of the selected continent countries = self.tz_dict[str(continent)] self.ui.countryList.clear() for country, zone in countries: self.ui.countryList.addItem(country, zone) self.countries.append(country) def dateChanged(self): self.is_date_changed = True def timerStop(self): if self.from_time_updater: return # Human action detected; stop the timer. self.timer.stop() def updateClock(self): # What time is it ? cur = QTime.currentTime() self.from_time_updater = True self.ui.timeEdit.setTime(cur) self.from_time_updater = False def shown(self): self.timer.start(1000) if ctx.flags.install_type == ctx.STEP_BASE: self.pthread = PThread(self, self.startInit, self.dummy) def dummy(self): pass def setTime(self): ctx.interface.informationWindow.update(_("Adjusting time settings")) date = self.ui.calendarWidget.date() time = self.ui.timeEdit.time() args = "%02d%02d%02d%02d%04d.%02d" % (date.month(), date.day(), time.hour(), time.minute(), date.year(), time.second()) # Set current date and time ctx.logger.debug("Date/Time setting to %s" % args) yali.util.run_batch("date", [args]) # Sync date time with hardware ctx.logger.debug("YALI's time is syncing with the system.") yali.util.run_batch("hwclock", ["--systohc"]) ctx.interface.informationWindow.hide() def execute(self): if not self.timer.isActive() or self.is_date_changed: QTimer.singleShot(500, self.setTime) self.timer.stop() index = self.ui.countryList.currentIndex() ctx.installData.timezone = self.ui.countryList.itemData(index).toString() ctx.logger.debug("Time zone selected as %s " % ctx.installData.timezone) if ctx.flags.install_type == ctx.STEP_BASE: #FIXME:Refactor hacky code ctx.installData.rootPassword = ctx.consts.default_password ctx.installData.hostName = yali.util.product_release() if ctx.storageInitialized: disks = filter(lambda d: not d.format.hidden, ctx.storage.disks) if len(disks) == 1: ctx.storage.clearPartDisks = [disks[0].name] ctx.mainScreen.step_increment = 2 else: ctx.mainScreen.step_increment = 1 return True else: self.pds_messagebox.setMessage(_("Storage Devices initialising...")) self.pds_messagebox.animate(start=MIDCENTER, stop=MIDCENTER) ctx.mainScreen.step_increment = 0 self.pthread.start() QTimer.singleShot(2, self.startStorageInitialize) return False return True def startInit(self): self.pds_messagebox.animate(start=MIDCENTER, stop=MIDCENTER) def startStorageInitialize(self): ctx.storageInitialized = yali.storage.initialize(ctx.storage, ctx.interface) self.initFinished() def initFinished(self): self.pds_messagebox.animate(start=CURRENT, stop=CURRENT, direction=OUT) disks = filter(lambda d: not d.format.hidden, ctx.storage.disks) if ctx.storageInitialized: if len(disks) == 1: ctx.storage.clearPartDisks = [disks[0].name] ctx.mainScreen.step_increment = 2 else: ctx.mainScreen.step_increment = 1 ctx.mainScreen.slotNext(dry_run=True) else: ctx.mainScreen.enableBack()
class Preview(QWidget): sync_requested = pyqtSignal(object, object) split_requested = pyqtSignal(object, object) split_start_requested = pyqtSignal() def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QVBoxLayout() self.setLayout(l) l.setContentsMargins(0, 0, 0, 0) self.view = WebView(self) self.view.page().sync_requested.connect(self.request_sync) self.view.page().split_requested.connect(self.request_split) self.view.page().loadFinished.connect(self.load_finished) self.inspector = self.view.inspector self.inspector.setPage(self.view.page()) l.addWidget(self.view) self.bar = QToolBar(self) l.addWidget(self.bar) ac = actions['auto-reload-preview'] ac.setCheckable(True) ac.setChecked(True) ac.toggled.connect(self.auto_reload_toggled) self.auto_reload_toggled(ac.isChecked()) self.bar.addAction(ac) ac = actions['sync-preview-to-editor'] ac.setCheckable(True) ac.setChecked(True) ac.toggled.connect(self.sync_toggled) self.sync_toggled(ac.isChecked()) self.bar.addAction(ac) self.bar.addSeparator() ac = actions['split-in-preview'] ac.setCheckable(True) ac.setChecked(False) ac.toggled.connect(self.split_toggled) self.split_toggled(ac.isChecked()) self.bar.addAction(ac) ac = actions['reload-preview'] ac.triggered.connect(self.refresh) self.bar.addAction(ac) actions['preview-dock'].toggled.connect(self.visibility_changed) self.current_name = None self.last_sync_request = None self.refresh_timer = QTimer(self) self.refresh_timer.timeout.connect(self.refresh) parse_worker.start() self.current_sync_request = None def request_sync(self, lnum): if self.current_name: self.sync_requested.emit(self.current_name, lnum) def request_split(self, loc): if self.current_name: self.split_requested.emit(self.current_name, loc) def sync_to_editor(self, name, lnum): self.current_sync_request = (name, lnum) QTimer.singleShot(100, self._sync_to_editor) def _sync_to_editor(self): if not actions['sync-preview-to-editor'].isChecked(): return try: if self.refresh_timer.isActive() or self.current_sync_request[0] != self.current_name: return QTimer.singleShot(100, self._sync_to_editor) except TypeError: return # Happens if current_sync_request is None lnum = self.current_sync_request[1] self.current_sync_request = None self.view.page().go_to_line(lnum) def show(self, name): if name != self.current_name: self.refresh_timer.stop() self.current_name = name parse_worker.add_request(name) self.view.setUrl(QUrl.fromLocalFile(current_container().name_to_abspath(name))) def refresh(self): if self.current_name: self.refresh_timer.stop() # This will check if the current html has changed in its editor, # and re-parse it if so parse_worker.add_request(self.current_name) # Tell webkit to reload all html and associated resources current_url = QUrl.fromLocalFile(current_container().name_to_abspath(self.current_name)) if current_url != self.view.url(): # The container was changed self.view.setUrl(current_url) else: self.view.refresh() def clear(self): self.view.clear() @property def is_visible(self): return actions['preview-dock'].isChecked() def start_refresh_timer(self): if self.is_visible and actions['auto-reload-preview'].isChecked(): self.refresh_timer.start(tprefs['preview_refresh_time'] * 1000) def stop_refresh_timer(self): self.refresh_timer.stop() def auto_reload_toggled(self, checked): actions['auto-reload-preview'].setToolTip(_( 'Auto reload preview when text changes in editor') if not checked else _( 'Disable auto reload of preview')) def sync_toggled(self, checked): actions['sync-preview-to-editor'].setToolTip(_( 'Disable syncing of preview position to editor position') if checked else _( 'Enable syncing of preview position to editor position')) def visibility_changed(self, is_visible): if is_visible: self.refresh() def split_toggled(self, checked): actions['split-in-preview'].setToolTip(textwrap.fill(_( 'Abort file split') if checked else _( 'Split this file at a specified location.\n\nAfter clicking this button, click' ' inside the preview panel above at the location you want the file to be split.'))) if checked: self.split_start_requested.emit() else: self.view.page().split_mode(False) def do_start_split(self): self.view.page().split_mode(True) def stop_split(self): actions['split-in-preview'].setChecked(False) def load_finished(self, ok): if actions['split-in-preview'].isChecked(): if ok: self.do_start_split() else: self.stop_split()