class TagBrowserWidget(QFrame): # {{{ def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle(QFrame.NoFrame if gprefs['tag_browser_old_look'] else QFrame.StyledPanel) self._parent = parent self._layout = QVBoxLayout(self) self._layout.setContentsMargins(0,0,0,0) # Set up the find box & button self.tb_bar = tbb = TagBrowserBar(self) self.alter_tb, self.item_search, self.search_button = tbb.alter_tb, tbb.item_search, tbb.search_button self.toggle_search_button = tbb.toggle_search_button self._layout.addWidget(tbb) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[str].connect(self.do_find) # The tags view parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self._layout.insertWidget(0, parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText('<p><b>'+_('No More Matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10,20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect(self.not_found_label_timer_event, type=Qt.QueuedConnection) # The Alter Tag Browser button l = self.alter_tb self.collapse_all_action = ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser collapse all', _('Collapse all'), default_keys=(), action=ac, group=_('Tag browser')) connect_lambda(ac.triggered, self, lambda self: self.tags_view.collapseAll()) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser alter', _('Configure Tag browser'), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(l.showMenu) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate((_('Name'), _('Number of books'), _('Average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip( _('Set the sort order for entries in the Tag browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate((_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc.')) mt.setToolTip(_('All of these category_managers are available by right-clicking ' 'on items in the tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser toggle item', _("'Click' found item"), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.toggle_item) # self.leak_test_timer = QTimer(self) # self.leak_test_timer.timeout.connect(self.test_for_leak) # self.leak_test_timer.start(5000) def save_state(self): gprefs.set('tag browser search box visible', self.toggle_search_button.isChecked()) def toggle_item(self): self.tags_view.toggle_current_index() def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) def find_text_changed(self, str): self.current_find_position = None def set_focus_to_find_box(self): self.tb_bar.set_focus_to_find_box() def do_find(self, str=None): self.current_find_position = None self.find() @property def find_text(self): return unicode_type(self.item_search.currentText()).strip() def find(self): model = self.tags_view.model() model.clear_boxed() txt = self.find_text if txt.startswith('*'): model.set_categories_filter(txt[1:]) self.tags_view.recount() self.current_find_position = None return if model.get_categories_filter(): model.set_categories_filter(None) self.tags_view.recount() self.current_find_position = None if not txt: return self.item_search.lineEdit().blockSignals(True) self.search_button.setFocus(True) self.item_search.lineEdit().blockSignals(False) key = None colon = txt.rfind(':') if len(txt) > 2 else 0 if colon > 0: key = self._parent.library_view.model().db.\ field_metadata.search_term_to_field_key(txt[:colon]) txt = txt[colon+1:] self.current_find_position = \ model.find_item_node(key, txt, self.current_find_position) if self.current_find_position: self.tags_view.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) if self.tags_view.verticalScrollBar().isVisible(): sbw = self.tags_view.verticalScrollBar().width() else: sbw = 0 width = self.width() - 8 - sbw height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) self.not_found_label.move(4, 10) self.not_found_label_timer.start(2000) def not_found_label_timer_event(self): self.not_found_label.setVisible(False) def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Enter, Qt.Key_Return) and self.find_text: self.find() ev.accept() return return QFrame.keyPressEvent(self, ev)
def get(self, qurl, html=None, num_retries=1, delay = 10, timeout = 10): t1 = time() loop = QEventLoop() timer = QTimer() timer.setSingleShot(True) timer.timeout.connect(loop.quit) self.loadFinished.connect(loop.quit) if qurl: if html: self.setHtml(html, qurl) else: self.mainFrame().load(QUrl(qurl)) timer.start(timeout * 1000) loop.exec_() # delay here until download finished or timeout if timer.isActive(): # downloaded successfully timer.stop() self._wait(delay - (time() - t1)) parsed_html = self.mainFrame().toHtml() else: # did not download in time if num_retries > 0: logging.debug('Timeout - retrying') parsed_html = self.get(qurl, num_retries=num_retries-1, timerout=timeout, delay=delay) else: logging.debug('Timed out') parsed_html = '' self.mainFrame().setHtml(None) return parsed_html
class DetailView(QDialog, Ui_Dialog): # {{{ def __init__(self, parent, job): QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle(job.description) self.job = job self.html_view = (hasattr(job, 'html_details') and not getattr(job, 'ignore_html_details', False)) if self.html_view: self.log.setVisible(False) else: self.tb.setVisible(False) self.next_pos = 0 self.update() self.timer = QTimer(self) self.timer.timeout.connect(self.update) self.timer.start(1000) v = self.log.verticalScrollBar() v.setValue(v.maximum()) def update(self): if self.html_view: html = self.job.html_details if len(html) > self.next_pos: self.next_pos = len(html) self.tb.setHtml( '<pre style="font-family:monospace">%s</pre>'%html) else: f = self.job.log_file f.seek(self.next_pos) more = f.read() self.next_pos = f.tell() if more: self.log.appendPlainText(more.decode('utf-8', 'replace'))
class ServerDialog(QDialog, Ui_ServerDialog): def startServerClicked(self): self.message("ServerDialog.startServerClicked - Start / threadId="+str(int(QThread.currentThreadId()))) self.worker.startServer(); def stopServerClicked(self): self.message("ServerDialog.stopServerClicked - Start / threadId="+str(int(QThread.currentThreadId()))) self.worker.stopServer(); @pyqtSlot('bool') def setConnectionStatus(self, enabled): if not enabled: self.startServerButton.clicked.disconnect(self.stopServerClicked) self.startServerButton.clicked.connect(self.startServerClicked) self.startServerButton.setText("Start server") else: self.startServerButton.clicked.disconnect(self.startServerClicked) self.startServerButton.clicked.connect(self.stopServerClicked) self.startServerButton.setText("Stop server") def message(self, str_): self.messageQueue.put(str_) def update(self): scrollToBottom = False; while self.messageQueue.empty()!=True: message = self.messageQueue.get() listItem = QListWidgetItem() listItem.setText(message) self.messageList.addItem(listItem) scrollToBottom = True; if scrollToBottom: self.messageList.scrollToBottom() def __init__(self): super().__init__() self.messageQueue = Queue() self.timer = QTimer(self) self.timer.timeout.connect(self.update) self.timer.start(250) self.workerThread = WorkThread() self.worker=Server(self.message) self.worker.isConnected.connect(self.setConnectionStatus) self.worker.moveToThread(self.workerThread) self.workerThread.start(QThread.HighPriority) self.setupUi(self) self.startServerButton.clicked.connect(self.startServerClicked) def closeEvent(self, event): self.worker.stopServer() # TODO: WAIT HERE UNTIL ARE THREADS ARE DEAD self.accept() super().closeEvent(event)
class DetailView(Dialog): # {{{ def __init__(self, parent, job): self.job = job self.html_view = hasattr(job, 'html_details') and not getattr(job, 'ignore_html_details', False) Dialog.__init__(self, job.description, 'job-detail-view-dialog', parent) def sizeHint(self): return QSize(700, 500) @property def plain_text(self): if self.html_view: return self.tb.toPlainText() return self.log.toPlainText() def copy_to_clipboard(self): QApplication.instance().clipboard().setText(self.plain_text) def setup_ui(self): self.l = l = QVBoxLayout(self) if self.html_view: self.tb = w = QTextBrowser(self) else: self.log = w = QPlainTextEdit(self) w.setReadOnly(True), w.setLineWrapMode(w.NoWrap) l.addWidget(w) l.addWidget(self.bb) self.bb.clear(), self.bb.setStandardButtons(self.bb.Close) self.copy_button = b = self.bb.addButton(_('&Copy to clipboard'), self.bb.ActionRole) b.setIcon(QIcon(I('edit-copy.png'))) b.clicked.connect(self.copy_to_clipboard) self.next_pos = 0 self.update() self.timer = QTimer(self) self.timer.timeout.connect(self.update) self.timer.start(1000) if not self.html_view: v = self.log.verticalScrollBar() v.setValue(v.maximum()) def update(self): if self.html_view: html = self.job.html_details if len(html) > self.next_pos: self.next_pos = len(html) self.tb.setHtml( '<pre style="font-family:monospace">%s</pre>'%html) else: f = self.job.log_file f.seek(self.next_pos) more = f.read() self.next_pos = f.tell() if more: self.log.appendPlainText(more.decode('utf-8', 'replace'))
class Canvas(QFrame): def __init__(self, parent): super().__init__(parent) self.initUI() self.timer = QTimer(self) self.timer.timeout.connect(self.update) self.timer.start(25) def initUI(self): self.resize(World.WIDTH, World.HEIGHT) self.setFrameStyle(QFrame.StyledPanel) self.setLineWidth(1) self.setStyleSheet("background-color:#FFFFFF;") label_stylesheet = """ .QLabel { font-weight: bold; font-size: 21px; color: red; border: 1px solid red; } """ self.message = QLabel("FRAME MODIFIED!", self) self.message.setStyleSheet(label_stylesheet) self.message.setAlignment(Qt.AlignCenter) self.message.resize(StickmanList.BUTTON_WIDTH, StickmanList.BUTTON_HEIGHT) self.message.move(World.WIDTH-StickmanList.BUTTON_WIDTH-15, World.HEIGHT-StickmanList.BUTTON_HEIGHT*1.5) self.message.hide() """ Draw and listener methods which are dispatched to the World """ def paintEvent(self, event): painter = QPainter() painter.begin(self) getWorld().draw(painter) painter.end() def showMessage(self): self.message.show() def hideMessage(self): self.message.hide() def mousePressEvent(self, event): getWorld().mousePressed(event.x(), event.y()) def mouseReleaseEvent(self, event): getWorld().mouseReleased(event.x(), event.y()) def mouseMoveEvent(self, event): getWorld().mouseMoved(event.x(), event.y())
class Clock(QWidget): WIDTH = 140 HEIGHT = 45 TIMER_STEP = 25 def __init__(self, parent): super().__init__(parent) self.initUI() def initUI(self): clock_stylesheet = """ .QLabel { padding-top: 10px; font-weight: bold; font-size: 25px; color:#ff5e5e; } """ self.accumulator = 0 self.time = QLabel("0:00.0", self) self.time.resize(Clock.WIDTH, Clock.HEIGHT) self.time.setStyleSheet(clock_stylesheet) self.time.setAlignment(Qt.AlignHCenter) self.timer = QTimer(self) self.timer.timeout.connect(self.updateTime) self.timer.task = None def startClock(self): self.timer.start(Clock.TIMER_STEP) def stopClock(self): self.timer.stop() def reset(self): self.accumulator = 0 self.time.setText("0:00.0") def updateTime(self): self.accumulator = self.accumulator + Clock.TIMER_STEP/100 self.time.setText(str(int(self.accumulator%6000/600)) +":"+str(int(self.accumulator%600/100))+str(int(self.accumulator%100/10))+"."+str(int(self.accumulator)%10)) if self.task != None: self.task()
class GarbageCollector(QObject): ''' Disable automatic garbage collection and instead collect manually every INTERVAL milliseconds. This is done to ensure that garbage collection only happens in the GUI thread, as otherwise Qt can crash. ''' INTERVAL = 5000 def __init__(self, parent, debug=False): QObject.__init__(self, parent) self.debug = debug self.timer = QTimer(self) self.timer.timeout.connect(self.check) self.threshold = gc.get_threshold() gc.disable() self.timer.start(self.INTERVAL) # gc.set_debug(gc.DEBUG_SAVEALL) def check(self): # return self.debug_cycles() l0, l1, l2 = gc.get_count() if self.debug: print ('gc_check called:', l0, l1, l2) if l0 > self.threshold[0]: num = gc.collect(0) if self.debug: print ('collecting gen 0, found:', num, 'unreachable') if l1 > self.threshold[1]: num = gc.collect(1) if self.debug: print ('collecting gen 1, found:', num, 'unreachable') if l2 > self.threshold[2]: num = gc.collect(2) if self.debug: print ('collecting gen 2, found:', num, 'unreachable') def debug_cycles(self): gc.collect() for obj in gc.garbage: print (obj, repr(obj), type(obj))
def test_accept_proxy(self): """Accept Proxy QDialog""" under_test = ProxyQDialog() under_test.initialize_dialog() timer = QTimer() timer.timeout.connect(under_test.accept_proxy) timer.start(0.5) # When all proxy settings are empties, dialog Accepted self.assertTrue(ProxyQDialog.Accepted == under_test.exec()) # Proxy address is set, dialog Accepted under_test.proxy_address.setText('http://127.0.0.1:8000') timer.start(0.5) self.assertTrue(ProxyQDialog.Accepted == under_test.exec())
def test_accept_login(self): """QDialog Accept Login""" under_test = LoginQDialog() under_test.create_widget() timer = QTimer() timer.timeout.connect(under_test.accept_login) timer.start(0.5) # If login failed, Rejected self.assertEqual(LoginQDialog.Rejected, under_test.exec()) # Set username and password for login under_test.username_line.setText('admin') under_test.password_line.setText('admin') timer.start(0.5) self.assertEqual(LoginQDialog.Accepted, under_test.exec())
class MoveMonitor(QObject): def __init__(self, worker, rq, callback, parent): QObject.__init__(self, parent) self.worker = worker self.rq = rq self.callback = callback self.parent = parent self.worker.start() self.dialog = ProgressDialog(_('Moving library...'), '', max=self.worker.total, parent=parent) self.dialog.button_box.setDisabled(True) self.dialog.setModal(True) self.dialog.show() self.timer = QTimer(self) self.timer.timeout.connect(self.check) self.timer.start(200) def check(self): if self.worker.is_alive(): self.update() else: self.timer.stop() self.dialog.hide() if self.worker.failed: error_dialog(self.parent, _('Failed to move library'), _('Failed to move library'), self.worker.details, show=True) return self.callback(None) else: return self.callback(self.worker.to) def update(self): try: title = self.rq.get_nowait()[-1] self.dialog.value += 1 self.dialog.set_msg(_('Copied') + ' '+title) except Empty: pass
class CoverDelegate(QStyledItemDelegate): # {{{ ICON_SIZE = 150, 200 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.dark_color = parent.palette().color(QPalette.WindowText) self.light_color = parent.palette().color(QPalette.Window) self.spinner_width = 64 def frame_changed(self, *args): self.angle = (self.angle-2)%360 self.needs_redraw.emit() def start_animation(self): self.angle = 0 self.timer.start(10) def stop_animation(self): self.timer.stop() def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, index) style = QApplication.style() waiting = self.timer.isActive() and bool(index.data(Qt.UserRole)) if waiting: rect = QRect(0, 0, self.spinner_width, self.spinner_width) rect.moveCenter(option.rect.center()) draw_snake_spinner(painter, rect, self.angle, self.light_color, self.dark_color) 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 External(): def __init__(self): self.watchTimer = QTimer() self.watchTimer.setInterval(800) self.watchTimer.timerEvent = self.onWatch self.files_changed = queue.Queue() pass def onWatch(self,e): try: file_type, filename, action = self.files_changed.get_nowait() print(file_type, filename, action) except queue.Empty: pass pass def onFileChanged(self, path=None): print('file changed', path) pass def load(self, path): print('load', path) Watcher(path, self.files_changed) if not self.watchTimer.isActive(): self.watchTimer.start() pass pass def update(self, path): pass def create(self, tmppath=None): if not tmppath: tmppath = 'tmp' if not os.path.exists(tmppath): os.mkdir(tmppath) pass pass
def test_edit_dialog_accept_text(self): """EditQDialog Accept Text""" under_test = EditQDialog() under_test.initialize('title', 'text to edit') under_test.text_edit.setText('text to edit') timer = QTimer() timer.timeout.connect(under_test.accept_text) timer.start(0.5) # Text is same so refused self.assertEqual(EditQDialog.Rejected, under_test.exec()) under_test.text_edit.setText('text have been edited') timer.start(0.5) # Accepted because text have changed self.assertEqual(EditQDialog.Accepted, under_test.exec()) # Reset text to empty and spaces under_test.old_text = '' under_test.text_edit.setText(' ') timer.start(0.5) # Rejected because there is nothing to change self.assertEqual(EditQDialog.Rejected, under_test.exec()) under_test.old_text = '' under_test.text_edit.setText('New text') timer.start(0.5) # Accepted even if old text is empty self.assertEqual(EditQDialog.Accepted, under_test.exec())
class PluginSwitch(QWidget): """ This function is responsible for initializing a log plugin. """ def __init__(self, window, grid, name, plugin_id, address): # Save the window data. self.address = address self.name = name self.plugin_id = plugin_id # Initialize this class. super(PluginSwitch, self).__init__() # Initialize a UDP socket for this plugin. self.udp_socket = socket(AF_INET, SOCK_DGRAM) # Set socket options. self.udp_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) self.udp_socket.settimeout(WV_REQ_TIMEOUT) # Initialize a timer to query data for this plugin. self.timer = QTimer() self.timer.timeout.connect(self.refresh); # Initialize update thread. self.refresh_thread = SwitchRefreshThread(self.plugin_id, self.address, self.udp_socket) self.refresh_thread.start() # Connect the data signal. self.refresh_thread.data_signal.connect(self.do_update) # Initialize UI for this plugin. self.PluginName = QtWidgets.QLabel(window) self.PluginName.setText(name) grid.addWidget(self.PluginName, grid.rowCount(), 0) RefreshSpacer = QtWidgets.QSpacerItem(65535, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum) grid.addItem(RefreshSpacer, (grid.rowCount() - 1), 1) self.RefreshLable = QtWidgets.QLabel(window) self.RefreshLable.setText("Refresh Time (ms) :") grid.addWidget(self.RefreshLable, (grid.rowCount() - 1), 2) self.Refresh = QtWidgets.QLineEdit(window) self.Refresh.setValidator(QDoubleValidator(0, 999999, 2)) self.Refresh.setMinimumWidth(50) self.Refresh.textChanged.connect(self.refersh_time_updated) self.Refresh.setText('500') grid.addWidget(self.Refresh, (grid.rowCount() - 1), 3) self.StateLable = QtWidgets.QLabel(window) self.StateLable.setText("Current State :") grid.addWidget(self.StateLable, grid.rowCount(), 0) self.State = QtWidgets.QLabel(window) self.State.setText("?") grid.addWidget(self.State, (grid.rowCount() - 1), 1) self.StateOn = QtWidgets.QPushButton(window) self.StateOn.setText("On") self.StateOn.clicked.connect(self.set_on) grid.addWidget(self.StateOn, (grid.rowCount() - 1), 2) self.StateOff = QtWidgets.QPushButton(window) self.StateOff.setText("Off") self.StateOff.clicked.connect(self.set_off) grid.addWidget(self.StateOff, (grid.rowCount() - 1), 3) """ This is callback function for refresh timer. """ def refresh(self): if DEBUG: print("Triggering an update for", self.plugin_id) # Wake the update thread. self.refresh_thread.wait.wakeAll() """ This is callback function for turn on button. """ def set_on(self): if DEBUG: print("Turned on requested for switch at", self.plugin_id) # Send an on request for this swicth. self.udp_socket.sendto(bytes.fromhex(WV_REQ) + bytes([((self.plugin_id & 0xFF00) >> 8), (self.plugin_id & 0x00FF)]) + bytes([WV_PLUGIN_SWITCH_ON]), self.address) """ This is callback function for turn off button. """ def set_off(self): if DEBUG: print("Turned off requested for switch at", self.plugin_id) # Send an off request for this swicth. self.udp_socket.sendto(bytes.fromhex(WV_REQ) + bytes([((self.plugin_id & 0xFF00) >> 8), (self.plugin_id & 0x00FF)]) + bytes([WV_PLUGIN_SWITCH_OFF]), self.address) """ This function is responsible for updating plugin display data. """ def do_update(self, state): if DEBUG: print("Got an update for", self.plugin_id) if state == True: if DEBUG: print("Switch turned on at", self.plugin_id) # We are now turned on. self.State.setText("On") else: if DEBUG: print("Switch turned off at", self.plugin_id) # We are now turned off. self.State.setText("Off") """ This is callback function when refresh period updates. """ def refersh_time_updated(self): refresh_time = 0 try: refresh_time = int(self.Refresh.text()); except: pass if refresh_time < 1: refresh_time = 1; # Reset the timer with new refersh time. self.timer.start(refresh_time);
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin, EbookDownloadMixin ): 'The main GUI' proceed_requested = pyqtSignal(object, object) book_converted = pyqtSignal(object, object) def __init__(self, opts, parent=None, gui_debug=None): global _gui MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True) self.jobs_pointer = Pointer(self) self.proceed_requested.connect(self.do_proceed, type=Qt.QueuedConnection) self.proceed_question = ProceedQuestion(self) self.job_error_dialog = JobError(self) self.keyboard = Manager(self) _gui = self self.opts = opts self.device_connected = None self.gui_debug = gui_debug self.iactions = OrderedDict() # Actions for action in interface_actions(): if opts.ignore_plugins and action.plugin_path is not None: continue try: ac = self.init_iaction(action) except: # Ignore errors in loading user supplied plugins import traceback traceback.print_exc() if action.plugin_path is None: raise continue ac.plugin_path = action.plugin_path ac.interface_action_base_plugin = action self.add_iaction(ac) self.load_store_plugins() def init_iaction(self, action): ac = action.load_actual_plugin(self) ac.plugin_path = action.plugin_path ac.interface_action_base_plugin = action action.actual_iaction_plugin_loaded = True return ac def add_iaction(self, ac): acmap = self.iactions if ac.name in acmap: if ac.priority >= acmap[ac.name].priority: acmap[ac.name] = ac else: acmap[ac.name] = ac def load_store_plugins(self): from calibre.gui2.store.loader import Stores self.istores = Stores() for store in available_store_plugins(): if self.opts.ignore_plugins and store.plugin_path is not None: continue try: st = self.init_istore(store) self.add_istore(st) except: # Ignore errors in loading user supplied plugins import traceback traceback.print_exc() if store.plugin_path is None: raise continue self.istores.builtins_loaded() def init_istore(self, store): st = store.load_actual_plugin(self) st.plugin_path = store.plugin_path st.base_plugin = store store.actual_istore_plugin_loaded = True return st def add_istore(self, st): stmap = self.istores if st.name in stmap: if st.priority >= stmap[st.name].priority: stmap[st.name] = st else: stmap[st.name] = st def initialize(self, library_path, db, listener, actions, show_gui=True): opts = self.opts self.preferences_action, self.quit_action = actions self.library_path = library_path self.content_server = None self._spare_pool = None self.must_restart_before_config = False self.listener = Listener(listener) self.check_messages_timer = QTimer() self.check_messages_timer.timeout.connect(self.another_instance_wants_to_talk) self.check_messages_timer.start(1000) for ac in self.iactions.values(): try: ac.do_genesis() except Exception: # Ignore errors in third party plugins import traceback traceback.print_exc() if getattr(ac, 'plugin_path', None) is None: raise self.donate_action = QAction(QIcon(I('donate.png')), _('&Donate to support calibre'), self) for st in self.istores.values(): st.do_genesis() MainWindowMixin.init_main_window_mixin(self, db) # Jobs Button {{{ self.job_manager = JobManager() self.jobs_dialog = JobsDialog(self, self.job_manager) self.jobs_button = JobsButton(horizontal=True, parent=self) self.jobs_button.initialize(self.jobs_dialog, self.job_manager) # }}} LayoutMixin.init_layout_mixin(self) DeviceMixin.init_device_mixin(self) self.progress_indicator = ProgressIndicator(self) self.progress_indicator.pos = (0, 20) self.verbose = opts.verbose self.get_metadata = GetMetadata() self.upload_memory = {} self.metadata_dialogs = [] self.default_thumbnail = None self.tb_wrapper = textwrap.TextWrapper(width=40) self.viewers = collections.deque() self.system_tray_icon = None if config['systray_icon']: self.system_tray_icon = factory(app_id='com.calibre-ebook.gui').create_system_tray_icon(parent=self, title='calibre') if self.system_tray_icon is not None: self.system_tray_icon.setIcon(QIcon(I('lt.png'))) self.system_tray_icon.setToolTip(self.jobs_button.tray_tooltip()) self.system_tray_icon.setVisible(True) self.jobs_button.tray_tooltip_updated.connect(self.system_tray_icon.setToolTip) elif config['systray_icon']: prints('Failed to create system tray icon, your desktop environment probably does not support the StatusNotifier spec') self.system_tray_menu = QMenu(self) self.toggle_to_tray_action = self.system_tray_menu.addAction(QIcon(I('page.png')), '') self.toggle_to_tray_action.triggered.connect(self.system_tray_icon_activated) self.system_tray_menu.addAction(self.donate_action) self.donate_button.setDefaultAction(self.donate_action) self.donate_button.setStatusTip(self.donate_button.toolTip()) self.eject_action = self.system_tray_menu.addAction( QIcon(I('eject.png')), _('&Eject connected device')) self.eject_action.setEnabled(False) self.addAction(self.quit_action) self.system_tray_menu.addAction(self.quit_action) self.keyboard.register_shortcut('quit calibre', _('Quit calibre'), default_keys=('Ctrl+Q',), action=self.quit_action) if self.system_tray_icon is not None: self.system_tray_icon.setContextMenu(self.system_tray_menu) self.system_tray_icon.activated.connect(self.system_tray_icon_activated) self.quit_action.triggered[bool].connect(self.quit) self.donate_action.triggered[bool].connect(self.donate) self.esc_action = QAction(self) self.addAction(self.esc_action) self.keyboard.register_shortcut('clear current search', _('Clear the current search'), default_keys=('Esc',), action=self.esc_action) self.esc_action.triggered.connect(self.esc) self.shift_esc_action = QAction(self) self.addAction(self.shift_esc_action) self.keyboard.register_shortcut('focus book list', _('Focus the book list'), default_keys=('Shift+Esc',), action=self.shift_esc_action) self.shift_esc_action.triggered.connect(self.shift_esc) self.ctrl_esc_action = QAction(self) self.addAction(self.ctrl_esc_action) self.keyboard.register_shortcut('clear virtual library', _('Clear the virtual library'), default_keys=('Ctrl+Esc',), action=self.ctrl_esc_action) self.ctrl_esc_action.triggered.connect(self.ctrl_esc) self.alt_esc_action = QAction(self) self.addAction(self.alt_esc_action) self.keyboard.register_shortcut('clear additional restriction', _('Clear the additional restriction'), default_keys=('Alt+Esc',), action=self.alt_esc_action) self.alt_esc_action.triggered.connect(self.clear_additional_restriction) # ###################### Start spare job server ######################## QTimer.singleShot(1000, self.create_spare_pool) # ###################### Location Manager ######################## self.location_manager.location_selected.connect(self.location_selected) self.location_manager.unmount_device.connect(self.device_manager.umount_device) self.location_manager.configure_device.connect(self.configure_connected_device) self.location_manager.update_device_metadata.connect(self.update_metadata_on_device) self.eject_action.triggered.connect(self.device_manager.umount_device) # ################### Update notification ################### UpdateMixin.init_update_mixin(self, opts) # ###################### Search boxes ######################## SearchRestrictionMixin.init_search_restirction_mixin(self) SavedSearchBoxMixin.init_saved_seach_box_mixin(self) # ###################### Library view ######################## LibraryViewMixin.init_library_view_mixin(self, db) SearchBoxMixin.init_search_box_mixin(self) # Requires current_db if show_gui: self.show() if self.system_tray_icon is not None and self.system_tray_icon.isVisible() and opts.start_in_tray: self.hide_windows() self.library_view.model().count_changed_signal.connect( self.iactions['Choose Library'].count_changed) if not gprefs.get('quick_start_guide_added', False): try: add_quick_start_guide(self.library_view) except: import traceback traceback.print_exc() for view in ('library', 'memory', 'card_a', 'card_b'): v = getattr(self, '%s_view' % view) v.selectionModel().selectionChanged.connect(self.update_status_bar) v.model().count_changed_signal.connect(self.update_status_bar) self.library_view.model().count_changed() self.bars_manager.database_changed(self.library_view.model().db) self.library_view.model().database_changed.connect(self.bars_manager.database_changed, type=Qt.QueuedConnection) # ########################## Tags Browser ############################## TagBrowserMixin.init_tag_browser_mixin(self, db) # ######################## Search Restriction ########################## if db.prefs['virtual_lib_on_startup']: self.apply_virtual_library(db.prefs['virtual_lib_on_startup']) self.rebuild_vl_tabs() # ########################## Cover Flow ################################ CoverFlowMixin.init_cover_flow_mixin(self) self._calculated_available_height = min(max_available_height()-15, self.height()) self.resize(self.width(), self._calculated_available_height) self.build_context_menus() for ac in self.iactions.values(): try: ac.gui_layout_complete() except: import traceback traceback.print_exc() if ac.plugin_path is None: raise if config['autolaunch_server']: self.start_content_server() self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.read_settings() self.finalize_layout() if self.bars_manager.showing_donate: self.donate_button.start_animation() self.set_window_title() for ac in self.iactions.values(): try: ac.initialization_complete() except: import traceback traceback.print_exc() if ac.plugin_path is None: raise self.set_current_library_information(current_library_name(), db.library_id, db.field_metadata) register_keyboard_shortcuts() self.keyboard.finalize() self.auto_adder = AutoAdder(gprefs['auto_add_path'], self) self.save_layout_state() # Collect cycles now gc.collect() if show_gui and self.gui_debug is not None: info_dialog(self, _('Debug mode'), '<p>' + _('You have started calibre in debug mode. After you ' 'quit calibre, the debug log will be available in ' 'the file: %s<p>The ' 'log will be displayed automatically.')%self.gui_debug, show=True) self.iactions['Connect Share'].check_smartdevice_menus() QTimer.singleShot(1, self.start_smartdevice) QTimer.singleShot(100, self.update_toggle_to_tray_action) def esc(self, *args): self.clear_button.click() def shift_esc(self): self.current_view().setFocus(Qt.OtherFocusReason) def ctrl_esc(self): self.apply_virtual_library() self.current_view().setFocus(Qt.OtherFocusReason) def start_smartdevice(self): message = None if self.device_manager.get_option('smartdevice', 'autostart'): try: message = self.device_manager.start_plugin('smartdevice') except: message = 'start smartdevice unknown exception' prints(message) import traceback traceback.print_exc() if message: if not self.device_manager.is_running('Wireless Devices'): error_dialog(self, _('Problem starting the wireless device'), _('The wireless device driver had problems starting. ' 'It said "%s"')%message, show=True) self.iactions['Connect Share'].set_smartdevice_action_state() def start_content_server(self, check_started=True): from calibre.library.server.main import start_threaded_server from calibre.library.server import server_config self.content_server = start_threaded_server( self.library_view.model().db, server_config().parse()) self.content_server.state_callback = Dispatcher( self.iactions['Connect Share'].content_server_state_changed) if check_started: self.content_server.start_failure_callback = \ Dispatcher(self.content_server_start_failed) def content_server_start_failed(self, msg): error_dialog(self, _('Failed to start Content Server'), _('Could not start the content server. Error:\n\n%s')%msg, show=True) def resizeEvent(self, ev): MainWindow.resizeEvent(self, ev) self.search.setMaximumWidth(self.width()-150) def create_spare_pool(self, *args): if self._spare_pool is None: num = min(detect_ncpus(), int(config['worker_limit']/2.0)) self._spare_pool = Pool(max_workers=num, name='GUIPool') def spare_pool(self): ans, self._spare_pool = self._spare_pool, None QTimer.singleShot(1000, self.create_spare_pool) return ans def do_proceed(self, func, payload): if callable(func): func(payload) def no_op(self, *args): pass def system_tray_icon_activated(self, r=False): if r in (QSystemTrayIcon.Trigger, QSystemTrayIcon.MiddleClick, False): if self.isVisible(): if self.isMinimized(): self.showNormal() else: self.hide_windows() else: self.show_windows() if self.isMinimized(): self.showNormal() @property def is_minimized_to_tray(self): return getattr(self, '__systray_minimized', False) def ask_a_yes_no_question(self, title, msg, det_msg='', show_copy_button=False, ans_when_user_unavailable=True, skip_dialog_name=None, skipped_value=True): if self.is_minimized_to_tray: return ans_when_user_unavailable return question_dialog(self, title, msg, det_msg=det_msg, show_copy_button=show_copy_button, skip_dialog_name=skip_dialog_name, skip_dialog_skipped_value=skipped_value) def update_toggle_to_tray_action(self, *args): if hasattr(self, 'toggle_to_tray_action'): self.toggle_to_tray_action.setText( _('Hide main window') if self.isVisible() else _('Show main window')) def hide_windows(self): for window in QApplication.topLevelWidgets(): if isinstance(window, (MainWindow, QDialog)) and \ window.isVisible(): window.hide() setattr(window, '__systray_minimized', True) self.update_toggle_to_tray_action() def show_windows(self, *args): for window in QApplication.topLevelWidgets(): if getattr(window, '__systray_minimized', False): window.show() setattr(window, '__systray_minimized', False) self.update_toggle_to_tray_action() def test_server(self, *args): if self.content_server is not None and \ self.content_server.exception is not None: error_dialog(self, _('Failed to start content server'), unicode(self.content_server.exception)).exec_() @property def current_db(self): return self.library_view.model().db def another_instance_wants_to_talk(self): try: msg = self.listener.queue.get_nowait() except Empty: return if msg.startswith('launched:'): import json try: argv = json.loads(msg[len('launched:'):]) except ValueError: prints('Failed to decode message from other instance: %r' % msg) if DEBUG: error_dialog(self, 'Invalid message', 'Received an invalid message from other calibre instance.' ' Do you have multiple versions of calibre installed?', det_msg='Invalid msg: %r' % msg, show=True) argv = () if isinstance(argv, (list, tuple)) and len(argv) > 1: files = [os.path.abspath(p) for p in argv[1:] if not os.path.isdir(p) and os.access(p, os.R_OK)] if files: self.iactions['Add Books'].add_filesystem_book(files) self.setWindowState(self.windowState() & ~Qt.WindowMinimized|Qt.WindowActive) self.show_windows() self.raise_() self.activateWindow() elif msg.startswith('refreshdb:'): m = self.library_view.model() m.db.new_api.reload_from_db() m.db.data.refresh(clear_caches=False, do_search=False) m.resort() m.research() self.tags_view.recount() elif msg.startswith('shutdown:'): self.quit(confirm_quit=False) elif msg.startswith('bookedited:'): parts = msg.split(':')[1:] try: book_id, fmt, library_id = parts[:3] book_id = int(book_id) m = self.library_view.model() db = m.db.new_api if m.db.library_id == library_id and db.has_id(book_id): db.format_metadata(book_id, fmt, allow_cache=False, update_db=True) db.update_last_modified((book_id,)) m.refresh_ids((book_id,)) except Exception: import traceback traceback.print_exc() else: print msg def current_view(self): '''Convenience method that returns the currently visible view ''' idx = self.stack.currentIndex() if idx == 0: return self.library_view if idx == 1: return self.memory_view if idx == 2: return self.card_a_view if idx == 3: return self.card_b_view def booklists(self): return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db def library_moved(self, newloc, copy_structure=False, call_close=True, allow_rebuild=False): if newloc is None: return default_prefs = None try: olddb = self.library_view.model().db if copy_structure: default_prefs = olddb.prefs from calibre.utils.formatter_functions import unload_user_template_functions unload_user_template_functions(olddb.library_id) except: olddb = None try: db = LibraryDatabase(newloc, default_prefs=default_prefs) except apsw.Error: if not allow_rebuild: raise import traceback repair = question_dialog(self, _('Corrupted database'), _('The library database at %s appears to be corrupted. Do ' 'you want calibre to try and rebuild it automatically? ' 'The rebuild may not be completely successful.') % force_unicode(newloc, filesystem_encoding), det_msg=traceback.format_exc() ) if repair: from calibre.gui2.dialogs.restore_library import repair_library_at if repair_library_at(newloc, parent=self): db = LibraryDatabase(newloc, default_prefs=default_prefs) else: return else: return if self.content_server is not None: self.content_server.set_database(db) self.library_path = newloc prefs['library_path'] = self.library_path self.book_on_device(None, reset=True) db.set_book_on_device_func(self.book_on_device) self.library_view.set_database(db) self.tags_view.set_database(db, self.alter_tb) self.library_view.model().set_book_on_device_func(self.book_on_device) self.status_bar.clear_message() self.search.clear() self.saved_search.clear() self.book_details.reset_info() # self.library_view.model().count_changed() db = self.library_view.model().db self.iactions['Choose Library'].count_changed(db.count()) self.set_window_title() self.apply_named_search_restriction('') # reset restriction to null self.saved_searches_changed(recount=False) # reload the search restrictions combo box if db.prefs['virtual_lib_on_startup']: self.apply_virtual_library(db.prefs['virtual_lib_on_startup']) self.rebuild_vl_tabs() for action in self.iactions.values(): action.library_changed(db) if olddb is not None: try: if call_close: olddb.close() except: import traceback traceback.print_exc() olddb.break_cycles() if self.device_connected: self.set_books_in_library(self.booklists(), reset=True) self.refresh_ondevice() self.memory_view.reset() self.card_a_view.reset() self.card_b_view.reset() self.set_current_library_information(current_library_name(), db.library_id, db.field_metadata) self.library_view.set_current_row(0) # Run a garbage collection now so that it does not freeze the # interface later gc.collect() def set_window_title(self): db = self.current_db restrictions = [x for x in (db.data.get_base_restriction_name(), db.data.get_search_restriction_name()) if x] restrictions = ' :: '.join(restrictions) font = QFont() if restrictions: restrictions = ' :: ' + restrictions font.setBold(True) font.setItalic(True) self.virtual_library.setFont(font) title = u'{0} - || {1}{2} ||'.format( __appname__, self.iactions['Choose Library'].library_name(), restrictions) self.setWindowTitle(title) def location_selected(self, location): ''' Called when a location icon is clicked (e.g. Library) ''' page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3 self.stack.setCurrentIndex(page) self.book_details.reset_info() for x in ('tb', 'cb'): splitter = getattr(self, x+'_splitter') splitter.button.setEnabled(location == 'library') for action in self.iactions.values(): action.location_selected(location) if location == 'library': self.virtual_library_menu.setEnabled(True) self.highlight_only_button.setEnabled(True) self.vl_tabs.setEnabled(True) else: self.virtual_library_menu.setEnabled(False) self.highlight_only_button.setEnabled(False) self.vl_tabs.setEnabled(False) # Reset the view in case something changed while it was invisible self.current_view().reset() self.set_number_of_books_shown() self.update_status_bar() def job_exception(self, job, dialog_title=_('Conversion Error'), retry_func=None): if not hasattr(self, '_modeless_dialogs'): self._modeless_dialogs = [] minz = self.is_minimized_to_tray if self.isVisible(): for x in list(self._modeless_dialogs): if not x.isVisible(): self._modeless_dialogs.remove(x) try: if 'calibre.ebooks.DRMError' in job.details: if not minz: from calibre.gui2.dialogs.drm_error import DRMErrorMessage d = DRMErrorMessage(self, _('Cannot convert') + ' ' + job.description.split(':')[-1].partition('(')[-1][:-1]) d.setModal(False) d.show() self._modeless_dialogs.append(d) return if 'calibre.ebooks.oeb.transforms.split.SplitError' in job.details: title = job.description.split(':')[-1].partition('(')[-1][:-1] msg = _('<p><b>Failed to convert: %s')%title msg += '<p>'+_(''' Many older ebook reader devices are incapable of displaying EPUB files that have internal components over a certain size. Therefore, when converting to EPUB, calibre automatically tries to split up the EPUB into smaller sized pieces. For some files that are large undifferentiated blocks of text, this splitting fails. <p>You can <b>work around the problem</b> by either increasing the maximum split size under EPUB Output in the conversion dialog, or by turning on Heuristic Processing, also in the conversion dialog. Note that if you make the maximum split size too large, your ebook reader may have trouble with the EPUB. ''') if not minz: d = error_dialog(self, _('Conversion Failed'), msg, det_msg=job.details) d.setModal(False) d.show() self._modeless_dialogs.append(d) return if 'calibre.web.feeds.input.RecipeDisabled' in job.details: if not minz: msg = job.details msg = msg[msg.find('calibre.web.feeds.input.RecipeDisabled:'):] msg = msg.partition(':')[-1] d = error_dialog(self, _('Recipe Disabled'), '<p>%s</p>'%msg) d.setModal(False) d.show() self._modeless_dialogs.append(d) return if 'calibre.ebooks.conversion.ConversionUserFeedBack:' in job.details: if not minz: import json payload = job.details.rpartition( 'calibre.ebooks.conversion.ConversionUserFeedBack:')[-1] payload = json.loads('{' + payload.partition('{')[-1]) d = {'info':info_dialog, 'warn':warning_dialog, 'error':error_dialog}.get(payload['level'], error_dialog) d = d(self, payload['title'], '<p>%s</p>'%payload['msg'], det_msg=payload['det_msg']) d.setModal(False) d.show() self._modeless_dialogs.append(d) return except: pass if job.killed: return try: prints(job.details, file=sys.stderr) except: pass if not minz: self.job_error_dialog.show_error(dialog_title, _('<b>Failed</b>')+': '+unicode(job.description), det_msg=job.details, retry_func=retry_func) def read_settings(self): geometry = config['main_window_geometry'] if geometry is not None: self.restoreGeometry(geometry) self.read_layout_settings() def write_settings(self): with gprefs: # Only write to gprefs once config.set('main_window_geometry', self.saveGeometry()) dynamic.set('sort_history', self.library_view.model().sort_history) self.save_layout_state() def quit(self, checked=True, restart=False, debug_on_restart=False, confirm_quit=True): if confirm_quit and not self.confirm_quit(): return try: self.shutdown() except: pass self.restart_after_quit = restart self.debug_on_restart = debug_on_restart QApplication.instance().quit() def donate(self, *args): open_url(QUrl('http://calibre-ebook.com/donate')) def confirm_quit(self): if self.job_manager.has_jobs(): msg = _('There are active jobs. Are you sure you want to quit?') if self.job_manager.has_device_jobs(): msg = '<p>'+__appname__ + \ _(''' is communicating with the device!<br> Quitting may cause corruption on the device.<br> Are you sure you want to quit?''')+'</p>' if not question_dialog(self, _('Active jobs'), msg): return False from calibre.db.delete_service import has_jobs if has_jobs(): msg = _('Some deleted books are still being moved to the Recycle ' 'Bin, if you quit now, they will be left behind. Are you ' 'sure you want to quit?') if not question_dialog(self, _('Active jobs'), msg): return False return True def shutdown(self, write_settings=True): self.grid_view.shutdown() try: db = self.library_view.model().db cf = db.clean except: pass else: cf() # Save the current field_metadata for applications like calibre2opds # Goes here, because if cf is valid, db is valid. db.new_api.set_pref('field_metadata', db.field_metadata.all_metadata()) db.commit_dirty_cache() db.prefs.write_serialized(prefs['library_path']) for action in self.iactions.values(): if not action.shutting_down(): return if write_settings: self.write_settings() self.check_messages_timer.stop() if hasattr(self, 'update_checker'): self.update_checker.shutdown() self.listener.close() self.job_manager.server.close() self.job_manager.threaded_server.close() self.device_manager.keep_going = False self.auto_adder.stop() mb = self.library_view.model().metadata_backup if mb is not None: mb.stop() self.hide_windows() try: try: if self.content_server is not None: s = self.content_server self.content_server = None s.exit() except: pass except KeyboardInterrupt: pass if self._spare_pool is not None: self._spare_pool.shutdown() from calibre.db.delete_service import shutdown shutdown() time.sleep(2) self.istores.join() self.hide_windows() # Do not report any errors that happen after the shutdown sys.excepthook = sys.__excepthook__ return True def run_wizard(self, *args): if self.confirm_quit(): self.run_wizard_b4_shutdown = True self.restart_after_quit = True try: self.shutdown(write_settings=False) except: pass QApplication.instance().quit() def closeEvent(self, e): self.write_settings() if self.system_tray_icon is not None and self.system_tray_icon.isVisible(): if not dynamic['systray_msg'] and not isosx: info_dialog(self, 'calibre', 'calibre '+ _('will keep running in the system tray. To close it, ' 'choose <b>Quit</b> in the context menu of the ' 'system tray.'), show_copy_button=False).exec_() dynamic['systray_msg'] = True self.hide_windows() e.ignore() else: if self.confirm_quit(): try: self.shutdown(write_settings=False) except: import traceback traceback.print_exc() e.accept() else: e.ignore()
class EbookViewer(MainWindow): STATE_VERSION = 2 FLOW_MODE_TT = _('Switch to paged mode - where the text is broken up ' 'into pages like a paper book') PAGED_MODE_TT = _('Switch to flow mode - where the text is not broken up ' 'into pages') AUTOSAVE_INTERVAL = 10 # seconds def __init__(self, pathtoebook=None, debug_javascript=False, open_at=None, start_in_fullscreen=False, continue_reading=False): MainWindow.__init__(self, debug_javascript) self.view.magnification_changed.connect(self.magnification_changed) self.closed = False self.show_toc_on_open = False self.current_book_has_toc = False self.iterator = None self.current_page = None self.pending_search = None self.pending_search_dir = None self.pending_anchor = None self.pending_reference = None self.pending_bookmark = None self.pending_restore = False self.pending_goto_page = None self.cursor_hidden = False self.existing_bookmarks = [] self.selected_text = None self.was_maximized = False self.page_position_on_footnote_toggle = [] self.read_settings() self.autosave_timer = t = QTimer(self) t.setInterval(self.AUTOSAVE_INTERVAL * 1000), t.setSingleShot(True) t.timeout.connect(self.autosave) self.pos.value_changed.connect(self.update_pos_label) self.pos.value_changed.connect(self.autosave_timer.start) self.pos.setMinimumWidth(150) self.setFocusPolicy(Qt.StrongFocus) self.view.set_manager(self) self.pi = ProgressIndicator(self) self.action_quit = QAction(_('&Quit'), self) self.addAction(self.action_quit) self.view_resized_timer = QTimer(self) self.view_resized_timer.timeout.connect(self.viewport_resize_finished) self.view_resized_timer.setSingleShot(True) self.resize_in_progress = False self.action_reload = QAction(_('&Reload book'), self) self.action_reload.triggered.connect(self.reload_book) self.action_quit.triggered.connect(self.quit) self.action_reference_mode.triggered[bool].connect( self.view.reference_mode) self.action_metadata.triggered[bool].connect(self.metadata.setVisible) self.action_table_of_contents.toggled[bool].connect( self.set_toc_visible) self.action_copy.triggered[bool].connect(self.copy) self.action_font_size_larger.triggered.connect(self.font_size_larger) self.action_font_size_smaller.triggered.connect(self.font_size_smaller) self.action_open_ebook.triggered[bool].connect(self.open_ebook) self.action_next_page.triggered.connect(self.view.next_page) self.action_previous_page.triggered.connect(self.view.previous_page) self.action_find_next.triggered.connect(self.find_next) self.action_find_previous.triggered.connect(self.find_previous) self.action_full_screen.triggered[bool].connect(self.toggle_fullscreen) self.action_back.triggered[bool].connect(self.back) self.action_forward.triggered[bool].connect(self.forward) self.action_preferences.triggered.connect(self.do_config) self.pos.editingFinished.connect(self.goto_page_num) self.vertical_scrollbar.valueChanged[int].connect( lambda x: self.goto_page(x / 100.)) self.search.search.connect(self.find) self.search.focus_to_library.connect( lambda: self.view.setFocus(Qt.OtherFocusReason)) self.toc.pressed[QModelIndex].connect(self.toc_clicked) self.toc.searched.connect(partial(self.toc_clicked, force=True)) self.reference.goto.connect(self.goto) self.bookmarks.edited.connect(self.bookmarks_edited) self.bookmarks.activated.connect(self.goto_bookmark) self.bookmarks.create_requested.connect(self.bookmark) self.set_bookmarks([]) self.load_theme_menu() if pathtoebook is not None: f = functools.partial(self.load_ebook, pathtoebook, open_at=open_at) QTimer.singleShot(50, f) elif continue_reading: QTimer.singleShot(50, self.continue_reading) self.window_mode_changed = None self.toggle_toolbar_action = QAction(_('Show/hide controls'), self) self.toggle_toolbar_action.setCheckable(True) self.toggle_toolbar_action.triggered.connect(self.toggle_toolbars) self.toolbar_hidden = None self.addAction(self.toggle_toolbar_action) self.full_screen_label_anim = QPropertyAnimation( self.full_screen_label, b'size') self.clock_timer = QTimer(self) self.clock_timer.timeout.connect(self.update_clock) self.action_print.triggered.connect(self.print_book) self.clear_recent_history_action = QAction( _('Clear list of recently opened books'), self) self.clear_recent_history_action.triggered.connect( self.clear_recent_history) self.build_recent_menu() self.open_history_menu.triggered.connect(self.open_recent) for x in ('tool_bar', 'tool_bar2'): x = getattr(self, x) for action in x.actions(): # So that the keyboard shortcuts for these actions will # continue to function even when the toolbars are hidden self.addAction(action) for plugin in self.view.document.all_viewer_plugins: plugin.customize_ui(self) self.view.document.settings_changed.connect(self.settings_changed) self.restore_state() self.settings_changed() self.action_toggle_paged_mode.toggled[bool].connect( self.toggle_paged_mode) if (start_in_fullscreen or self.view.document.start_in_fullscreen): self.action_full_screen.trigger() self.hide_cursor_timer = t = QTimer(self) t.setSingleShot(True), t.setInterval(3000) t.timeout.connect(self.hide_cursor) t.start() def eventFilter(self, obj, ev): if ev.type() == ev.MouseMove: if self.cursor_hidden: self.cursor_hidden = False QApplication.instance().restoreOverrideCursor() self.hide_cursor_timer.start() return False def hide_cursor(self): self.cursor_hidden = True QApplication.instance().setOverrideCursor(Qt.BlankCursor) def toggle_paged_mode(self, checked, at_start=False): in_paged_mode = not self.action_toggle_paged_mode.isChecked() self.view.document.in_paged_mode = in_paged_mode self.action_toggle_paged_mode.setToolTip( self.FLOW_MODE_TT if self.action_toggle_paged_mode.isChecked( ) else self.PAGED_MODE_TT) if at_start: return self.reload() def settings_changed(self): for x in ('', '2'): x = getattr(self, 'tool_bar' + x) x.setVisible(self.view.document.show_controls) def reload(self): if hasattr(self, 'current_index') and self.current_index > -1: self.view.document.page_position.save(overwrite=False) self.pending_restore = True self.load_path(self.view.last_loaded_path) def set_toc_visible(self, yes): self.toc_dock.setVisible(yes) if not yes: self.show_toc_on_open = False def clear_recent_history(self, *args): vprefs.set('viewer_open_history', []) self.build_recent_menu() def build_recent_menu(self): m = self.open_history_menu m.clear() recent = vprefs.get('viewer_open_history', []) if recent: m.addAction(self.clear_recent_history_action) m.addSeparator() count = 0 for path in recent: if count > 9: break if os.path.exists(path): m.addAction(RecentAction(path, m)) count += 1 def continue_reading(self): actions = self.open_history_menu.actions()[2:] if actions: actions[0].trigger() def shutdown(self): if self.isFullScreen() and not self.view.document.start_in_fullscreen: self.action_full_screen.trigger() return False self.save_state() return True def quit(self): if self.shutdown(): QApplication.instance().quit() def closeEvent(self, e): if self.closed: e.ignore() return if self.shutdown(): self.closed = True return MainWindow.closeEvent(self, e) else: e.ignore() def toggle_toolbars(self): for x in ('tool_bar', 'tool_bar2'): x = getattr(self, x) x.setVisible(not x.isVisible()) def save_state(self): state = bytearray(self.saveState(self.STATE_VERSION)) vprefs['main_window_state'] = state if not self.isFullScreen(): vprefs.set('viewer_window_geometry', bytearray(self.saveGeometry())) if self.current_book_has_toc: vprefs.set( 'viewer_toc_isvisible', self.show_toc_on_open or bool(self.toc_dock.isVisible())) vprefs['multiplier'] = self.view.multiplier vprefs['in_paged_mode'] = not self.action_toggle_paged_mode.isChecked() def restore_state(self): state = vprefs.get('main_window_state', None) if state is not None: try: state = QByteArray(state) self.restoreState(state, self.STATE_VERSION) except: pass self.initialize_dock_state() mult = vprefs.get('multiplier', None) if mult: self.view.multiplier = mult # On windows Qt lets the user hide toolbars via a right click in a very # specific location, ensure they are visible. self.tool_bar.setVisible(True) self.tool_bar2.setVisible(True) self.toc_dock.close( ) # This will be opened on book open, if the book has a toc and it was previously opened self.action_toggle_paged_mode.setChecked( not vprefs.get('in_paged_mode', True)) self.toggle_paged_mode(self.action_toggle_paged_mode.isChecked(), at_start=True) def lookup(self, word): from urllib import quote word = quote(word.encode('utf-8')) try: url = lookup_website( canonicalize_lang(self.view.current_language) or 'en').format(word=word) except Exception: traceback.print_exc() url = default_lookup_website( canonicalize_lang(self.view.current_language) or 'en').format(word=word) open_url(url) def print_book(self): if self.iterator is None: return error_dialog(self, _('No book opened'), _('Cannot print as no book is opened'), show=True) from calibre.gui2.viewer.printing import print_book print_book(self.iterator.pathtoebook, self, self.current_title) def toggle_fullscreen(self): if self.isFullScreen(): self.showNormal() else: self.showFullScreen() def showFullScreen(self): self.view.document.page_position.save() self.window_mode_changed = 'fullscreen' self.tool_bar.setVisible(False) self.tool_bar2.setVisible(False) self.was_maximized = self.isMaximized() if not self.view.document.fullscreen_scrollbar: self.vertical_scrollbar.setVisible(False) super(EbookViewer, self).showFullScreen() def show_full_screen_label(self): f = self.full_screen_label height = f.final_height width = int(0.7 * self.view.width()) f.resize(width, height) if self.view.document.show_fullscreen_help: f.setVisible(True) a = self.full_screen_label_anim a.setDuration(500) a.setStartValue(QSize(width, 0)) a.setEndValue(QSize(width, height)) a.start() QTimer.singleShot(3500, self.full_screen_label.hide) if self.view.document.fullscreen_clock: self.show_clock() if self.view.document.fullscreen_pos: self.show_pos_label() self.relayout_fullscreen_labels() def show_clock(self): self.clock_label.setVisible(True) self.clock_label.setText( QTime(22, 33, 33).toString(Qt.SystemLocaleShortDate)) self.clock_timer.start(1000) self.clock_label.setStyleSheet( self.info_label_style % ('rgba(0, 0, 0, 0)', self.view.document.colors()[1])) self.clock_label.resize(self.clock_label.sizeHint()) self.update_clock() def show_pos_label(self): self.pos_label.setVisible(True) self.pos_label.setStyleSheet( self.info_label_style % ('rgba(0, 0, 0, 0)', self.view.document.colors()[1])) self.update_pos_label() def relayout_fullscreen_labels(self): vswidth = (self.vertical_scrollbar.width() if self.vertical_scrollbar.isVisible() else 0) p = self.pos_label p.move(15, p.parent().height() - p.height() - 10) c = self.clock_label c.move(c.parent().width() - vswidth - 15 - c.width(), c.parent().height() - c.height() - 10) f = self.full_screen_label f.move((f.parent().width() - f.width()) // 2, (f.parent().height() - f.final_height) // 2) def update_clock(self): self.clock_label.setText(QTime.currentTime().toString( Qt.SystemLocaleShortDate)) def update_pos_label(self, *args): if self.pos_label.isVisible(): try: value, maximum = args except: value, maximum = self.pos.value(), self.pos.maximum() text = '%g/%g' % (value, maximum) self.pos_label.setText(text) self.pos_label.resize(self.pos_label.sizeHint()) def showNormal(self): self.view.document.page_position.save() self.clock_label.setVisible(False) self.pos_label.setVisible(False) self.clock_timer.stop() self.vertical_scrollbar.setVisible(True) self.window_mode_changed = 'normal' self.settings_changed() self.full_screen_label.setVisible(False) if self.was_maximized: super(EbookViewer, self).showMaximized() else: super(EbookViewer, self).showNormal() def goto(self, ref): if ref: tokens = ref.split('.') if len(tokens) > 1: spine_index = int(tokens[0]) - 1 if spine_index == self.current_index: self.view.goto(ref) else: self.pending_reference = ref self.load_path(self.iterator.spine[spine_index]) def goto_bookmark(self, bm): spine_index = bm['spine'] if spine_index > -1 and self.current_index == spine_index: if self.resize_in_progress: self.view.document.page_position.set_pos(bm['pos']) else: self.view.goto_bookmark(bm) # Going to a bookmark does not call scrolled() so we update the # page position explicitly. Use a timer to ensure it is # accurate. QTimer.singleShot(100, self.update_page_number) else: self.pending_bookmark = bm if spine_index < 0 or spine_index >= len(self.iterator.spine): spine_index = 0 self.pending_bookmark = None self.load_path(self.iterator.spine[spine_index]) def toc_clicked(self, index, force=False): if force or QApplication.mouseButtons() & Qt.LeftButton: item = self.toc_model.itemFromIndex(index) if item.abspath is not None: if not os.path.exists(item.abspath): return error_dialog( self, _('No such location'), _('The location pointed to by this item' ' does not exist.'), det_msg=item.abspath, show=True) url = QUrl.fromLocalFile(item.abspath) if item.fragment: url.setFragment(item.fragment) self.link_clicked(url) self.view.setFocus(Qt.OtherFocusReason) def selection_changed(self, selected_text): self.selected_text = selected_text.strip() self.action_copy.setEnabled(bool(self.selected_text)) def copy(self, x): if self.selected_text: QApplication.clipboard().setText(self.selected_text) def back(self, x): pos = self.history.back(self.pos.value()) if pos is not None: self.goto_page(pos) def goto_page_num(self): num = self.pos.value() self.goto_page(num) def forward(self, x): pos = self.history.forward(self.pos.value()) if pos is not None: self.goto_page(pos) def goto_start(self): self.goto_page(1) def goto_end(self): self.goto_page(self.pos.maximum()) def goto_page(self, new_page, loaded_check=True): if self.current_page is not None or not loaded_check: for page in self.iterator.spine: if new_page >= page.start_page and new_page <= page.max_page: try: frac = float(new_page - page.start_page) / (page.pages - 1) except ZeroDivisionError: frac = 0 if page == self.current_page: self.view.scroll_to(frac) else: self.load_path(page, pos=frac) def open_ebook(self, checked): files = choose_files(self, 'ebook viewer open dialog', _('Choose ebook'), [(_('Ebooks'), available_input_formats())], all_files=False, select_only_single_file=True) if files: self.load_ebook(files[0]) def open_recent(self, action): self.load_ebook(action.path) def font_size_larger(self): self.view.magnify_fonts() def font_size_smaller(self): self.view.shrink_fonts() def magnification_changed(self, val): tt = '%(action)s [%(sc)s]\n' + _('Current magnification: %(mag).1f') sc = _(' or ').join(self.view.shortcuts.get_shortcuts('Font larger')) self.action_font_size_larger.setToolTip( tt % dict(action=unicode(self.action_font_size_larger.text()), mag=val, sc=sc)) sc = _(' or ').join(self.view.shortcuts.get_shortcuts('Font smaller')) self.action_font_size_smaller.setToolTip( tt % dict(action=unicode(self.action_font_size_smaller.text()), mag=val, sc=sc)) self.action_font_size_larger.setEnabled(self.view.multiplier < 3) self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2) def find(self, text, repeat=False, backwards=False): if not text: self.view.search('') return self.search.search_done(False) if self.view.search(text, backwards=backwards): self.scrolled(self.view.scroll_fraction) return self.search.search_done(True) index = self.iterator.search(text, self.current_index, backwards=backwards) if index is None: if self.current_index > 0: index = self.iterator.search(text, 0) if index is None: info_dialog(self, _('No matches found'), _('No matches found for: %s') % text).exec_() return self.search.search_done(True) return self.search.search_done(True) self.pending_search = text self.pending_search_dir = 'backwards' if backwards else 'forwards' self.load_path(self.iterator.spine[index]) def find_next(self): self.find(unicode(self.search.text()), repeat=True) def find_previous(self): self.find(unicode(self.search.text()), repeat=True, backwards=True) def do_search(self, text, backwards): self.pending_search = None self.pending_search_dir = None if self.view.search(text, backwards=backwards): self.scrolled(self.view.scroll_fraction) def internal_link_clicked(self, prev_pos): self.history.add(prev_pos) def link_clicked(self, url): path = os.path.abspath(unicode(url.toLocalFile())) frag = None if path in self.iterator.spine: self.update_page_number( ) # Ensure page number is accurate as it is used for history self.history.add(self.pos.value()) path = self.iterator.spine[self.iterator.spine.index(path)] if url.hasFragment(): frag = unicode(url.fragment()) if path != self.current_page: self.pending_anchor = frag self.load_path(path) else: oldpos = self.view.document.ypos if frag: self.view.scroll_to(frag) else: # Scroll to top self.view.scroll_to(0) if self.view.document.ypos == oldpos: # If we are coming from goto_next_section() call this will # cause another goto next section call with the next toc # entry, since this one did not cause any scrolling at all. QTimer.singleShot(10, self.update_indexing_state) else: open_url(url) def load_started(self): self.open_progress_indicator(_('Loading flow...')) def load_finished(self, ok): self.close_progress_indicator() path = self.view.path() try: index = self.iterator.spine.index(path) except (ValueError, AttributeError): return -1 self.current_page = self.iterator.spine[index] self.current_index = index self.set_page_number(self.view.scroll_fraction) QTimer.singleShot(100, self.update_indexing_state) if self.pending_search is not None: self.do_search(self.pending_search, self.pending_search_dir == 'backwards') self.pending_search = None self.pending_search_dir = None if self.pending_anchor is not None: self.view.scroll_to(self.pending_anchor) self.pending_anchor = None if self.pending_reference is not None: self.view.goto(self.pending_reference) self.pending_reference = None if self.pending_bookmark is not None: self.goto_bookmark(self.pending_bookmark) self.pending_bookmark = None if self.pending_restore: self.view.document.page_position.restore() return self.current_index def goto_next_section(self): if hasattr(self, 'current_index'): entry = self.toc_model.next_entry( self.current_index, self.view.document.read_anchor_positions(), self.view.viewport_rect, self.view.document.in_paged_mode) if entry is not None: self.pending_goto_next_section = ( self.toc_model.currently_viewed_entry, entry, False) self.toc_clicked(entry.index(), force=True) def goto_previous_section(self): if hasattr(self, 'current_index'): entry = self.toc_model.next_entry( self.current_index, self.view.document.read_anchor_positions(), self.view.viewport_rect, self.view.document.in_paged_mode, backwards=True) if entry is not None: self.pending_goto_next_section = ( self.toc_model.currently_viewed_entry, entry, True) self.toc_clicked(entry.index(), force=True) def update_indexing_state(self, anchor_positions=None): pgns = getattr(self, 'pending_goto_next_section', None) if hasattr(self, 'current_index'): if anchor_positions is None: anchor_positions = self.view.document.read_anchor_positions() items = self.toc_model.update_indexing_state( self.current_index, self.view.viewport_rect, anchor_positions, self.view.document.in_paged_mode) if items: self.toc.scrollTo(items[-1].index()) if pgns is not None: self.pending_goto_next_section = None # Check that we actually progressed if pgns[0] is self.toc_model.currently_viewed_entry: entry = self.toc_model.next_entry( self.current_index, self.view.document.read_anchor_positions(), self.view.viewport_rect, self.view.document.in_paged_mode, backwards=pgns[2], current_entry=pgns[1]) if entry is not None: self.pending_goto_next_section = ( self.toc_model.currently_viewed_entry, entry, pgns[2]) self.toc_clicked(entry.index(), force=True) def load_path(self, path, pos=0.0): self.open_progress_indicator(_('Laying out %s') % self.current_title) self.view.load_path(path, pos=pos) def footnote_visibility_changed(self, is_visible): if self.view.document.in_paged_mode: pp = namedtuple( 'PagePosition', 'time is_visible page_dimensions multiplier last_loaded_path page_number after_resize_page_number' ) self.page_position_on_footnote_toggle.append( pp(time.time(), is_visible, self.view.document.page_dimensions, self.view.multiplier, self.view.last_loaded_path, self.view.document.page_number, None)) def pre_footnote_toggle_position(self): num = len(self.page_position_on_footnote_toggle) if self.view.document.in_paged_mode and num > 1 and num % 2 == 0: two, one = self.page_position_on_footnote_toggle.pop( ), self.page_position_on_footnote_toggle.pop() if (time.time() - two.time < 1 and not two.is_visible and one.is_visible and one.last_loaded_path == two.last_loaded_path and two.last_loaded_path == self.view.last_loaded_path and one.page_dimensions == self.view.document.page_dimensions and one.multiplier == self.view.multiplier and one.after_resize_page_number == self.view.document.page_number): return one.page_number def viewport_resize_started(self, event): if not self.resize_in_progress: # First resize, so save the current page position self.resize_in_progress = True if not self.window_mode_changed: # The special handling for window mode changed will already # have saved page position, so only save it if this is not a # mode change self.view.document.page_position.save() if self.resize_in_progress: self.view_resized_timer.start(75) def viewport_resize_finished(self): # There hasn't been a resize event for some time # restore the current page position. self.resize_in_progress = False wmc, self.window_mode_changed = self.window_mode_changed, None fs = wmc == 'fullscreen' if wmc: # Sets up body text margins, which can be limited in fs mode by a # separate config option, so must be done before relayout of text (self.view.document.switch_to_fullscreen_mode if fs else self.view.document.switch_to_window_mode)() # Re-layout text, must be done before restoring page position self.view.document.after_resize() if wmc: # This resize is part of a window mode change, special case it if fs: self.show_full_screen_label() self.view.document.page_position.restore() self.scrolled(self.view.scroll_fraction) else: if self.isFullScreen(): self.relayout_fullscreen_labels() pre_footnote_pos = self.pre_footnote_toggle_position() if pre_footnote_pos is not None: self.view.document.page_number = pre_footnote_pos else: self.view.document.page_position.restore() self.update_page_number() if len(self.page_position_on_footnote_toggle) % 2 == 1: self.page_position_on_footnote_toggle[ -1] = self.page_position_on_footnote_toggle[ -1]._replace(after_resize_page_number=self.view. document.page_number) if self.pending_goto_page is not None: pos, self.pending_goto_page = self.pending_goto_page, None self.goto_page(pos, loaded_check=False) def update_page_number(self): self.set_page_number(self.view.document.scroll_fraction) return self.pos.value() def close_progress_indicator(self): self.pi.stop() for o in ('tool_bar', 'tool_bar2', 'view', 'horizontal_scrollbar', 'vertical_scrollbar'): getattr(self, o).setEnabled(True) self.unsetCursor() self.view.setFocus(Qt.PopupFocusReason) def open_progress_indicator(self, msg=''): self.pi.start(msg) for o in ('tool_bar', 'tool_bar2', 'view', 'horizontal_scrollbar', 'vertical_scrollbar'): getattr(self, o).setEnabled(False) self.setCursor(Qt.BusyCursor) def load_theme_menu(self): from calibre.gui2.viewer.config import load_themes self.themes_menu.clear() for key in load_themes(): title = key[len('theme_'):] self.themes_menu.addAction(title, partial(self.load_theme, key)) def load_theme(self, theme_id): self.view.load_theme(theme_id) def do_config(self): self.view.config(self) self.load_theme_menu() if self.iterator is not None: self.iterator.copy_bookmarks_to_file = self.view.document.copy_bookmarks_to_file from calibre.gui2 import config if not config['viewer_search_history']: self.search.clear_history() def bookmark(self, *args): num = 1 bm = None while True: bm = _('Bookmark #%d') % num if bm not in self.existing_bookmarks: break num += 1 title, ok = QInputDialog.getText(self, _('Add bookmark'), _('Enter title for bookmark:'), text=bm) title = unicode(title).strip() if ok and title: bm = self.view.bookmark() bm['spine'] = self.current_index bm['title'] = title self.iterator.add_bookmark(bm) self.set_bookmarks(self.iterator.bookmarks) self.bookmarks.set_current_bookmark(bm) def autosave(self): self.save_current_position(no_copy_to_file=True) def bookmarks_edited(self, bookmarks): self.build_bookmarks_menu(bookmarks) self.iterator.set_bookmarks(bookmarks) self.iterator.save_bookmarks() def build_bookmarks_menu(self, bookmarks): self.bookmarks_menu.clear() sc = _(' or ').join(self.view.shortcuts.get_shortcuts('Bookmark')) self.bookmarks_menu.addAction( _("Bookmark this location [%s]") % sc, self.bookmark) self.bookmarks_menu.addAction( _("Show/hide Bookmarks"), self.bookmarks_dock.toggleViewAction().trigger) self.bookmarks_menu.addSeparator() current_page = None self.existing_bookmarks = [] for bm in bookmarks: if bm['title'] == 'calibre_current_page_bookmark': if self.view.document.remember_current_page: current_page = bm else: self.existing_bookmarks.append(bm['title']) self.bookmarks_menu.addAction(bm['title'], partial(self.goto_bookmark, bm)) return current_page def set_bookmarks(self, bookmarks): self.bookmarks.set_bookmarks(bookmarks) return self.build_bookmarks_menu(bookmarks) @property def current_page_bookmark(self): bm = self.view.bookmark() bm['spine'] = self.current_index bm['title'] = 'calibre_current_page_bookmark' return bm def save_current_position(self, no_copy_to_file=False): if not self.view.document.remember_current_page: return if hasattr(self, 'current_index'): try: self.iterator.add_bookmark(self.current_page_bookmark, no_copy_to_file=no_copy_to_file) except: traceback.print_exc() def load_ebook(self, pathtoebook, open_at=None, reopen_at=None): if self.iterator is not None: self.save_current_position() self.iterator.__exit__() self.iterator = EbookIterator( pathtoebook, copy_bookmarks_to_file=self.view.document.copy_bookmarks_to_file) self.history.clear() self.open_progress_indicator(_('Loading ebook...')) worker = Worker( target=partial(self.iterator.__enter__, view_kepub=True)) worker.start() while worker.isAlive(): worker.join(0.1) QApplication.processEvents() if worker.exception is not None: if isinstance(worker.exception, DRMError): from calibre.gui2.dialogs.drm_error import DRMErrorMessage DRMErrorMessage(self).exec_() else: r = getattr(worker.exception, 'reason', worker.exception) error_dialog(self, _('Could not open ebook'), as_unicode(r) or _('Unknown error'), det_msg=worker.traceback, show=True) self.close_progress_indicator() else: self.metadata.show_opf(self.iterator.opf, self.iterator.book_format) self.view.current_language = self.iterator.language title = self.iterator.opf.title if not title: title = os.path.splitext(os.path.basename(pathtoebook))[0] if self.iterator.toc: self.toc_model = TOC(self.iterator.spine, self.iterator.toc) self.toc.setModel(self.toc_model) if self.show_toc_on_open: self.action_table_of_contents.setChecked(True) else: self.toc_model = TOC(self.iterator.spine) self.toc.setModel(self.toc_model) self.action_table_of_contents.setChecked(False) if isbytestring(pathtoebook): pathtoebook = force_unicode(pathtoebook, filesystem_encoding) vh = vprefs.get('viewer_open_history', []) try: vh.remove(pathtoebook) except: pass vh.insert(0, pathtoebook) vprefs.set('viewer_open_history', vh[:50]) self.build_recent_menu() self.footnotes_dock.close() self.action_table_of_contents.setDisabled(not self.iterator.toc) self.current_book_has_toc = bool(self.iterator.toc) self.current_title = title self.setWindowTitle(title + ' [%s]' % self.iterator.book_format + ' - ' + self.base_window_title) self.pos.setMaximum(sum(self.iterator.pages)) self.pos.setSuffix(' / %d' % sum(self.iterator.pages)) self.vertical_scrollbar.setMinimum(100) self.vertical_scrollbar.setMaximum(100 * sum(self.iterator.pages)) self.vertical_scrollbar.setSingleStep(10) self.vertical_scrollbar.setPageStep(100) self.set_vscrollbar_value(1) self.current_index = -1 QApplication.instance().alert(self, 5000) previous = self.set_bookmarks(self.iterator.bookmarks) if reopen_at is not None: previous = reopen_at if open_at is None and previous is not None: self.goto_bookmark(previous) else: if open_at is None: self.next_document() else: if open_at > self.pos.maximum(): open_at = self.pos.maximum() if open_at < self.pos.minimum(): open_at = self.pos.minimum() if self.resize_in_progress: self.pending_goto_page = open_at else: self.goto_page(open_at, loaded_check=False) def set_vscrollbar_value(self, pagenum): self.vertical_scrollbar.blockSignals(True) self.vertical_scrollbar.setValue(int(pagenum * 100)) self.vertical_scrollbar.blockSignals(False) def set_page_number(self, frac): if getattr(self, 'current_page', None) is not None: page = self.current_page.start_page + frac * float( self.current_page.pages - 1) self.pos.set_value(page) self.set_vscrollbar_value(page) def scrolled(self, frac, onload=False): self.set_page_number(frac) if not onload: ap = self.view.document.read_anchor_positions() self.update_indexing_state(ap) def next_document(self): if (hasattr(self, 'current_index') and self.current_index < len(self.iterator.spine) - 1): self.load_path(self.iterator.spine[self.current_index + 1]) def previous_document(self): if hasattr(self, 'current_index') and self.current_index > 0: self.load_path(self.iterator.spine[self.current_index - 1], pos=1.0) def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: if self.metadata.isVisible(): self.metadata.setVisible(False) event.accept() return if self.isFullScreen(): self.action_full_screen.trigger() event.accept() return try: key = self.view.shortcuts.get_match(event) except AttributeError: return MainWindow.keyPressEvent(self, event) try: bac = self.bookmarks_menu.actions()[0] except (AttributeError, TypeError, IndexError, KeyError): bac = None action = { 'Quit': self.action_quit, 'Show metadata': self.action_metadata, 'Copy': self.view.copy_action, 'Font larger': self.action_font_size_larger, 'Font smaller': self.action_font_size_smaller, 'Fullscreen': self.action_full_screen, 'Find next': self.action_find_next, 'Find previous': self.action_find_previous, 'Search online': self.view.search_online_action, 'Lookup word': self.view.dictionary_action, 'Next occurrence': self.view.search_action, 'Bookmark': bac, 'Reload': self.action_reload, 'Table of Contents': self.action_table_of_contents, 'Print': self.action_print, }.get(key, None) if action is not None: event.accept() action.trigger() return if key == 'Focus Search': self.search.setFocus(Qt.OtherFocusReason) return if not self.view.handle_key_press(event): event.ignore() def reload_book(self): if getattr(self.iterator, 'pathtoebook', None): try: reopen_at = self.current_page_bookmark except Exception: reopen_at = None self.history.clear() self.load_ebook(self.iterator.pathtoebook, reopen_at=reopen_at) return def __enter__(self): return self def __exit__(self, *args): if self.iterator is not None: self.save_current_position() self.iterator.__exit__(*args) def read_settings(self): c = config().parse() if c.remember_window_size: wg = vprefs.get('viewer_window_geometry', None) if wg is not None: self.restoreGeometry(wg) self.show_toc_on_open = vprefs.get('viewer_toc_isvisible', False) desktop = QApplication.instance().desktop() av = desktop.availableGeometry(self).height() - 30 if self.height() > av: self.resize(self.width(), av) def show_footnote_view(self): self.footnotes_dock.show()
class SearchBox2(QComboBox): # {{{ ''' To use this class: * Call initialize() * Connect to the search() and cleared() signals from this widget. * Connect to the changed() signal to know when the box content changes * Connect to focus_to_library() signal to be told to manually change focus * Call search_done() after every search is complete * Call set_search_string() to perform a search programmatically * You can use the current_text property to get the current search text Be aware that if you are using it in a slot connected to the changed() signal, if the connection is not queued it will not be accurate. ''' INTERVAL = 1500 #: Time to wait before emitting search signal MAX_COUNT = 25 search = pyqtSignal(object) cleared = pyqtSignal() changed = pyqtSignal() focus_to_library = pyqtSignal() def __init__(self, parent=None): QComboBox.__init__(self, parent) self.normal_background = 'rgb(255, 255, 255, 0%)' self.line_edit = SearchLineEdit(self) self.setLineEdit(self.line_edit) c = self.line_edit.completer() c.setCompletionMode(c.PopupCompletion) c.highlighted[str].connect(self.completer_used) self.line_edit.key_pressed.connect(self.key_pressed, type=Qt.DirectConnection) # QueuedConnection as workaround for https://bugreports.qt-project.org/browse/QTBUG-40807 self.activated[str].connect(self.history_selected, type=Qt.QueuedConnection) self.setEditable(True) self.as_you_type = True self.timer = QTimer() self.timer.setSingleShot(True) self.timer.timeout.connect(self.timer_event, type=Qt.QueuedConnection) self.setInsertPolicy(self.NoInsert) self.setMaxCount(self.MAX_COUNT) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setMinimumContentsLength(25) self._in_a_search = False self.tool_tip_text = self.toolTip() def initialize(self, opt_name, colorize=False, help_text=_('Search')): self.as_you_type = config['search_as_you_type'] self.opt_name = opt_name items = [] for item in config[opt_name]: if item not in items: items.append(item) self.addItems(items) self.line_edit.setPlaceholderText(help_text) self.colorize = colorize self.clear() def hide_completer_popup(self): try: self.lineEdit().completer().popup().setVisible(False) except: pass def normalize_state(self): self.setToolTip(self.tool_tip_text) self.line_edit.setStyleSheet( 'QLineEdit{color:none;background-color:%s;}' % self.normal_background) def text(self): return self.currentText() def clear_history(self, *args): QComboBox.clear(self) def clear(self, emit_search=True): self.normalize_state() self.setEditText('') if emit_search: self.search.emit('') self._in_a_search = False self.cleared.emit() def clear_clicked(self, *args): self.clear() def search_done(self, ok): if isinstance(ok, basestring): self.setToolTip(ok) ok = False if not unicode(self.currentText()).strip(): self.clear(emit_search=False) return self._in_a_search = ok col = 'rgba(0,255,0,20%)' if ok else 'rgb(255,0,0,20%)' if not self.colorize: col = self.normal_background self.line_edit.setStyleSheet('QLineEdit{color:black;background-color:%s;}' % col) # Comes from the lineEdit control def key_pressed(self, event): k = event.key() if k in (Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End, Qt.Key_PageUp, Qt.Key_PageDown, Qt.Key_unknown): return self.normalize_state() if self._in_a_search: self.changed.emit() self._in_a_search = False if event.key() in (Qt.Key_Return, Qt.Key_Enter): self.do_search() self.focus_to_library.emit() elif self.as_you_type and unicode(event.text()): self.timer.start(1500) # Comes from the combobox itself def keyPressEvent(self, event): k = event.key() if k in (Qt.Key_Enter, Qt.Key_Return): return self.do_search() if k not in (Qt.Key_Up, Qt.Key_Down): QComboBox.keyPressEvent(self, event) else: self.blockSignals(True) self.normalize_state() QComboBox.keyPressEvent(self, event) self.blockSignals(False) def completer_used(self, text): self.timer.stop() self.normalize_state() def timer_event(self): self.do_search() def history_selected(self, text): self.changed.emit() self.do_search() def _do_search(self, store_in_history=True): self.hide_completer_popup() text = unicode(self.currentText()).strip() if not text: return self.clear() self.search.emit(text) if store_in_history: idx = self.findText(text, Qt.MatchFixedString|Qt.MatchCaseSensitive) self.block_signals(True) if idx < 0: self.insertItem(0, text) else: t = self.itemText(idx) self.removeItem(idx) self.insertItem(0, t) self.setCurrentIndex(0) self.block_signals(False) history = [unicode(self.itemText(i)) for i in range(self.count())] config[self.opt_name] = history def do_search(self, *args): self._do_search() def block_signals(self, yes): self.blockSignals(yes) self.line_edit.blockSignals(yes) def set_search_string(self, txt, store_in_history=False, emit_changed=True): if not store_in_history: self.activated[str].disconnect() try: self.setFocus(Qt.OtherFocusReason) if not txt: self.clear() else: self.normalize_state() # must turn on case sensitivity here so that tag browser strings # are not case-insensitively replaced from history self.line_edit.completer().setCaseSensitivity(Qt.CaseSensitive) self.setEditText(txt) self.line_edit.end(False) if emit_changed: self.changed.emit() self._do_search(store_in_history=store_in_history) self.line_edit.completer().setCaseSensitivity(Qt.CaseInsensitive) self.focus_to_library.emit() finally: if not store_in_history: # QueuedConnection as workaround for https://bugreports.qt-project.org/browse/QTBUG-40807 self.activated[str].connect(self.history_selected, type=Qt.QueuedConnection) def search_as_you_type(self, enabled): self.as_you_type = enabled def in_a_search(self): return self._in_a_search @property def current_text(self): return unicode(self.lineEdit().text())
class TagBrowserWidget(QWidget): # {{{ def __init__(self, parent): QWidget.__init__(self, parent) self.parent = parent self._layout = QVBoxLayout() self.setLayout(self._layout) self._layout.setContentsMargins(0,0,0,0) # Set up the find box & button search_layout = QHBoxLayout() self._layout.addLayout(search_layout) self.item_search = HistoryLineEdit(parent) self.item_search.setMinimumContentsLength(5) self.item_search.setSizeAdjustPolicy(self.item_search.AdjustToMinimumContentsLengthWithIcon) try: self.item_search.lineEdit().setPlaceholderText( _('Find item in tag browser')) except: pass # Using Qt < 4.7 self.item_search.setToolTip(_( 'Search for items. This is a "contains" search; items containing the\n' 'text anywhere in the name will be found. You can limit the search\n' 'to particular categories using syntax similar to search. For example,\n' 'tags:foo will find foo in any tag, but not in authors etc. Entering\n' '*foo will filter all categories at once, showing only those items\n' 'containing the text "foo"')) search_layout.addWidget(self.item_search) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser find box', _('Find item'), default_keys=(), action=ac, group=_('Tag Browser')) ac.triggered.connect(self.set_focus_to_find_box) self.search_button = QToolButton() self.search_button.setText(_('Find')) self.search_button.setToolTip(_('Find the first/next matching item')) search_layout.addWidget(self.search_button) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser find button', _('Find button'), default_keys=(), action=ac, group=_('Tag Browser')) ac.triggered.connect(self.search_button.click) self.expand_button = QToolButton() self.expand_button.setText('-') self.expand_button.setToolTip(_('Collapse all categories')) search_layout.addWidget(self.expand_button) search_layout.setStretch(0, 10) search_layout.setStretch(1, 1) search_layout.setStretch(2, 1) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser collapse all', _('Collapse all'), default_keys=(), action=ac, group=_('Tag Browser')) ac.triggered.connect(self.expand_button.clicked) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.initialize('tag_browser_search') self.item_search.lineEdit().returnPressed.connect(self.do_find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[str].connect(self.do_find) self.item_search.completer().setCaseSensitivity(Qt.CaseSensitive) parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self.expand_button.clicked.connect(self.tags_view.collapseAll) self._layout.addWidget(parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText('<p><b>'+_('No More Matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10,20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect(self.not_found_label_timer_event, type=Qt.QueuedConnection) parent.alter_tb = l = QPushButton(parent) l.setText(_('Alter Tag Browser')) l.setIcon(QIcon(I('tags.png'))) l.m = QMenu() l.setMenu(l.m) self._layout.addWidget(l) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser alter', _('Alter tag browser'), default_keys=(), action=ac, group=_('Tag Browser')) ac.triggered.connect(l.showMenu) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate((_('Sort by name'), _('Sort by number of books'), _('Sort by average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip( _('Set the sort order for entries in the Tag Browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate((_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag Browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc.')) mt.setToolTip(_('All of these category_managers are available by right-clicking ' 'on items in the tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser toggle item', _("'Click' found item"), default_keys=(), action=ac, group=_('Tag Browser')) ac.triggered.connect(self.toggle_item) # self.leak_test_timer = QTimer(self) # self.leak_test_timer.timeout.connect(self.test_for_leak) # self.leak_test_timer.start(5000) def toggle_item(self): self.tags_view.toggle_current_index() def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) def find_text_changed(self, str): self.current_find_position = None def set_focus_to_find_box(self): self.item_search.setFocus() self.item_search.lineEdit().selectAll() def do_find(self, str=None): self.current_find_position = None self.find() def find(self): model = self.tags_view.model() model.clear_boxed() txt = unicode(self.item_search.currentText()).strip() if txt.startswith('*'): model.set_categories_filter(txt[1:]) self.tags_view.recount() self.current_find_position = None return if model.get_categories_filter(): model.set_categories_filter(None) self.tags_view.recount() self.current_find_position = None if not txt: return self.item_search.lineEdit().blockSignals(True) self.search_button.setFocus(True) self.item_search.lineEdit().blockSignals(False) key = None colon = txt.rfind(':') if len(txt) > 2 else 0 if colon > 0: key = self.parent.library_view.model().db.\ field_metadata.search_term_to_field_key(txt[:colon]) txt = txt[colon+1:] self.current_find_position = \ model.find_item_node(key, txt, self.current_find_position) if self.current_find_position: self.tags_view.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) if self.tags_view.verticalScrollBar().isVisible(): sbw = self.tags_view.verticalScrollBar().width() else: sbw = 0 width = self.width() - 8 - sbw height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) self.not_found_label.move(4, 10) self.not_found_label_timer.start(2000) def not_found_label_timer_event(self): self.not_found_label.setVisible(False)
class HostQWidget(QWidget): """ Class who create QWidget to display host data """ def __init__(self, parent=None): super(HostQWidget, self).__init__(parent) # Fields self.actions_widget = ActionsQWidget() self.host_item = None self.service_items = None self.labels = { 'host_icon': QLabel(), 'host_name': QLabel(), 'state_icon': QLabel(), 'ls_last_check': QLabel(), 'ls_output': QLabel(), 'realm': QLabel(), 'address': QLabel(), 'business_impact': QLabel(), 'notes': QLabel() } self.activecheck_btn = ToggleQWidgetButton() self.passivecheck_btn = ToggleQWidgetButton() self.history_btn = QPushButton() self.history_widget = HistoryQWidget() self.host_history = None self.customs_widget = CustomsQWidget() self.customs_btn = QPushButton() self.spy_btn = QPushButton() self.refresh_timer = QTimer() self.refresh_counter = 0 def initialize(self): """ Initialize QWidget """ layout = QGridLayout() self.setLayout(layout) # Add Qwidgets layout.addWidget(self.get_host_icon_widget(), 0, 0, 2, 1) layout.addWidget(self.get_last_check_widget(), 0, 1, 1, 1) layout.addWidget(self.get_variables_widget(), 0, 2, 1, 1) layout.addWidget(self.get_notes_output_widget(), 1, 1, 1, 2) layout.addWidget(self.get_actions_widget(), 0, 3, 2, 1) update_host = int(settings.get_config('Alignak-app', 'update_host')) * 1000 self.refresh_timer.setInterval(update_host) self.refresh_timer.start() self.refresh_timer.timeout.connect(self.update_host) def set_data(self, host_item): """ Set data of host and service :param host_item: the Host item :type host_item: alignak_app.items.host.Host """ # Query services of host self.host_item = host_item # Get problems host_and_services = data_manager.get_host_with_services(host_item.item_id) self.host_item = host_and_services['host'] self.service_items = host_and_services['services'] def get_host_icon_widget(self): """ Return QWidget with overall icon state and host name :return: widget with host name and icon :rtype: QWidget """ widget = QWidget() layout = QVBoxLayout() widget.setLayout(layout) # Host Icon layout.addWidget(self.labels['host_icon']) layout.setAlignment(self.labels['host_icon'], Qt.AlignCenter) # Host Name self.labels['host_name'].setObjectName('itemname') self.labels['host_name'].setWordWrap(True) layout.addWidget(self.labels['host_name']) layout.setAlignment(self.labels['host_name'], Qt.AlignCenter) # Customs button customs_lbl = QLabel(_('Configuration:')) customs_lbl.setObjectName('subtitle') layout.addWidget(customs_lbl) layout.setAlignment(customs_lbl, Qt.AlignBottom) self.customs_btn.setIcon(QIcon(settings.get_image('settings'))) self.customs_btn.setFixedSize(80, 20) self.customs_btn.clicked.connect(self.show_customs) layout.addWidget(self.customs_btn) layout.setAlignment(self.customs_btn, Qt.AlignCenter) # Initialize Customs QWidget self.customs_widget.initialize() return widget def get_actions_widget(self): """ Return QWidget with actions buttons :return: widget with buttons :rtype: QWidget """ widget = QWidget() layout = QVBoxLayout() widget.setLayout(layout) # Actions action_title = QLabel(_('Host actions')) action_title.setObjectName('itemtitle') action_title.setFixedHeight(25) layout.addWidget(action_title) ack_down_lbl = QLabel(_('Acknowledge / Downtime:')) ack_down_lbl.setObjectName('subtitle') layout.addWidget(ack_down_lbl) self.actions_widget.initialize(self.host_item) layout.addWidget(self.actions_widget) # Active Checks activecheck_lbl = QLabel(_('Active checks:')) activecheck_lbl.setObjectName('subtitle') layout.addWidget(activecheck_lbl) self.activecheck_btn.initialize() self.activecheck_btn.toggle_btn.clicked.connect(lambda: self.patch_host_checks( 'active_checks_enabled', self.activecheck_btn.is_checked() )) layout.addWidget(self.activecheck_btn) # Passive Checks passivecheck_lbl = QLabel(_('Passive checks:')) passivecheck_lbl.setObjectName('subtitle') layout.addWidget(passivecheck_lbl) self.passivecheck_btn.initialize() self.passivecheck_btn.toggle_btn.clicked.connect(lambda: self.patch_host_checks( 'passive_checks_enabled', self.passivecheck_btn.is_checked() )) layout.addWidget(self.passivecheck_btn) # History hist_lbl = QLabel(_('Timeline:')) hist_lbl.setObjectName('subtitle') layout.addWidget(hist_lbl) self.history_btn.setIcon(QIcon(settings.get_image('time'))) self.history_btn.setFixedSize(80, 20) self.history_btn.clicked.connect(self.show_history) self.history_btn.setToolTip(_('See history of host')) layout.addWidget(self.history_btn) layout.setAlignment(self.history_btn, Qt.AlignCenter) self.history_widget.initialize() # Spy Button spy_lbl = QLabel(_('Spy Host:')) spy_lbl.setObjectName('subtitle') layout.addWidget(spy_lbl) self.spy_btn.setIcon(QIcon(settings.get_image('spy'))) self.spy_btn.setFixedSize(80, 20) self.spy_btn.setToolTip(_('Spy current host')) layout.addWidget(self.spy_btn) layout.setAlignment(self.spy_btn, Qt.AlignCenter) layout.setAlignment(Qt.AlignCenter | Qt.AlignTop) return widget def get_last_check_widget(self): """ Return QWidget with last check data :return: widget with last check data :rtype: QWidget """ widget = QWidget() layout = QGridLayout() widget.setLayout(layout) # Title check_title = QLabel(_('My last check')) check_title.setObjectName('itemtitle') check_title.setFixedHeight(25) layout.addWidget(check_title, 0, 0, 1, 2) # State state_title = QLabel(_("State:")) state_title.setObjectName('subtitle') layout.addWidget(state_title, 1, 0, 1, 1) layout.addWidget(self.labels['state_icon'], 1, 1, 1, 1) # When last check when_title = QLabel(_("When:")) when_title.setObjectName('subtitle') layout.addWidget(when_title, 2, 0, 1, 1) layout.addWidget(self.labels['ls_last_check'], 2, 1, 1, 1) return widget def get_variables_widget(self): """ Return QWidget with host variables :return: widget with host variables :rtype: QWidget """ widget = QWidget() layout = QGridLayout() widget.setLayout(layout) # Title check_title = QLabel(_('My variables')) check_title.setObjectName('itemtitle') check_title.setFixedHeight(25) layout.addWidget(check_title, 0, 0, 1, 2) # Realm realm_title = QLabel(_("Realm:")) realm_title.setObjectName('subtitle') layout.addWidget(realm_title, 1, 0, 1, 1) layout.addWidget(self.labels['realm'], 1, 1, 1, 1) # Address address_title = QLabel(_("Host address:")) address_title.setObjectName('subtitle') layout.addWidget(address_title, 2, 0, 1, 1) layout.addWidget(self.labels['address'], 2, 1, 1, 1) # Business impact business_title = QLabel(_("Business impact:")) business_title.setObjectName('subtitle') layout.addWidget(business_title, 3, 0, 1, 1) layout.addWidget(self.labels['business_impact'], 3, 1, 1, 1) return widget def get_notes_output_widget(self): """ Return QWidget with output and notes data :return: widget with host output and notes :rtype: QWidget """ widget = QWidget() layout = QGridLayout() widget.setLayout(layout) # Output output_title = QLabel(_("Output")) output_title.setObjectName('title') layout.addWidget(output_title, 0, 0, 1, 1) self.labels['ls_output'].setWordWrap(True) self.labels['ls_output'].setTextInteractionFlags(Qt.TextSelectableByMouse) output_scrollarea = QScrollArea() output_scrollarea.setWidget(self.labels['ls_output']) output_scrollarea.setWidgetResizable(True) output_scrollarea.setObjectName('output') layout.addWidget(output_scrollarea, 1, 0, 1, 2) # Notes notes_title = QLabel(_("Notes:")) notes_title.setObjectName('title') layout.addWidget(notes_title, 0, 2, 1, 1) notes_btn = QPushButton() notes_btn.setIcon(QIcon(settings.get_image('edit'))) notes_btn.setToolTip(_("Edit host notes.")) notes_btn.setFixedSize(32, 32) notes_btn.clicked.connect(self.patch_data) layout.addWidget(notes_btn, 0, 3, 1, 1) self.labels['notes'].setWordWrap(True) self.labels['notes'].setTextInteractionFlags(Qt.TextSelectableByMouse) notes_scrollarea = QScrollArea() notes_scrollarea.setWidget(self.labels['notes']) notes_scrollarea.setWidgetResizable(True) notes_scrollarea.setObjectName('notes') layout.addWidget(notes_scrollarea, 1, 2, 1, 2) return widget def show_history(self): """ Update and show HistoryQWidget """ self.history_widget.update_history_data(self.host_item.name, self.host_history) self.history_widget.show() def show_customs(self): """ Update and show CustomsQWidget """ self.customs_widget.update_customs(self.host_item) self.customs_widget.show() def patch_host_checks(self, check_type, state): # pragma: no cover """ Patch the host check: 'active_checks_enabled' | 'passive_checks_enabled' :param check_type: type of check: 'active_checks_enabled' | 'passive_checks_enabled' :type check_type: str :param state: state of Toggle button :type state: bool """ data = {check_type: state} headers = {'If-Match': self.host_item.data['_etag']} endpoint = '/'.join([self.host_item.item_type, self.host_item.item_id]) patched = app_backend.patch(endpoint, data, headers) if patched: self.host_item.data[check_type] = state data_manager.update_item_data( self.host_item.item_type, self.host_item.item_id, self.host_item.data ) enabled = _('enabled') if state else _('disabled') event_type = 'OK' if state else 'WARN' message = _( _('%s %s for %s' % (Item.get_check_text(check_type), enabled, self.host_item.get_display_name())) ) send_event(event_type, message, timer=True) else: send_event( 'ERROR', _("Backend PATCH failed, please check your logs !") ) def patch_data(self): # pragma: no cover """ Display QDialog for patch """ notes_dialog = EditQDialog() notes_dialog.initialize( _('Edit Host Notes'), self.host_item.data['notes'] ) if notes_dialog.exec_() == EditQDialog.Accepted: data = {'notes': str(notes_dialog.text_edit.toPlainText())} headers = {'If-Match': self.host_item.data['_etag']} endpoint = '/'.join(['host', self.host_item.item_id]) patched = app_backend.patch(endpoint, data, headers) if patched: data_manager.update_item_data( self.host_item.item_type, self.host_item.item_id, {'notes': notes_dialog.text_edit.toPlainText()} ) self.labels['notes'].setText(notes_dialog.text_edit.toPlainText()) message = _( _("Host notes have been edited.") ) send_event('INFO', message) else: send_event( 'ERROR', _("Backend PATCH failed, please check your logs !") ) def update_host(self, host_item=None): """ Update HostQWidget data and QLabels :param host_item: the Host item :type host_item: alignak_app.items.host.Host """ if self.host_item and not host_item: self.set_data(self.host_item) if host_item: self.set_data(host_item) if self.host_item or host_item: # Update host services self.refresh_counter += 1 if self.refresh_counter > 10: thread_manager.add_high_priority_thread('service', self.host_item.item_id) self.refresh_counter = 0 # Update host icon_name = get_overall_state_icon( self.service_items, self.host_item.data['_overall_state_id'] ) icon_pixmap = QPixmap(settings.get_image(icon_name)) self.labels['host_icon'].setPixmap(QPixmap(icon_pixmap)) self.labels['host_icon'].setToolTip( self.host_item.get_overall_tooltip(self.service_items) ) self.labels['host_name'].setText('%s' % self.host_item.get_display_name()) monitored = self.host_item.data[ 'passive_checks_enabled'] + self.host_item.data['active_checks_enabled'] icon_name = get_icon_name( 'host', self.host_item.data['ls_state'], self.host_item.data['ls_acknowledged'], self.host_item.data['ls_downtimed'], monitored ) pixmap_icon = QPixmap(settings.get_image(icon_name)) final_icon = pixmap_icon.scaled(32, 32, Qt.KeepAspectRatio) self.labels['state_icon'].setPixmap(final_icon) self.labels['state_icon'].setToolTip(self.host_item.get_tooltip()) since_last_check = get_diff_since_last_timestamp( self.host_item.data['ls_last_check'] ) last_check_tooltip = get_date_fromtimestamp(self.host_item.data['ls_last_check']) self.labels['ls_last_check'].setText(since_last_check) self.labels['ls_last_check'].setToolTip(last_check_tooltip) self.labels['ls_output'].setText(self.host_item.data['ls_output']) self.labels['realm'].setText( data_manager.get_realm_name(self.host_item.data['_realm']) ) self.labels['address'].setText(self.host_item.data['address']) self.labels['business_impact'].setText(str(self.host_item.data['business_impact'])) self.labels['notes'].setText(self.host_item.data['notes']) self.actions_widget.item = self.host_item self.actions_widget.update_widget() self.activecheck_btn.update_btn_state(self.host_item.data['active_checks_enabled']) self.passivecheck_btn.update_btn_state(self.host_item.data['passive_checks_enabled']) self.customs_btn.setEnabled(bool(self.host_item.data['customs'])) # Update host history self.host_history = data_manager.get_item('history', self.host_item.item_id) if self.host_history: self.history_btn.setEnabled(True) self.history_btn.setToolTip(_('History is available')) else: self.history_btn.setToolTip(_('History is not available, please wait...')) self.history_btn.setEnabled(False) if app_backend.connected: thread_manager.add_high_priority_thread( 'history', {'hostname': self.host_item.name, 'host_id': self.host_item.item_id} ) else: thread_manager.stop_threads()
class Scheduler(QObject): INTERVAL = 1 # minutes delete_old_news = pyqtSignal(object) start_recipe_fetch = pyqtSignal(object) def __init__(self, parent, db): QObject.__init__(self, parent) self.internet_connection_failed = False self._parent = parent self.no_internet_msg = _('Cannot download news as no internet connection ' 'is active') self.no_internet_dialog = d = error_dialog(self._parent, self.no_internet_msg, _('No internet connection'), show_copy_button=False) d.setModal(False) self.recipe_model = RecipeModel() self.db = db self.lock = QMutex(QMutex.Recursive) self.download_queue = set([]) self.news_menu = QMenu() self.news_icon = QIcon(I('news.png')) self.scheduler_action = QAction(QIcon(I('scheduler.png')), _('Schedule news download'), self) self.news_menu.addAction(self.scheduler_action) self.scheduler_action.triggered[bool].connect(self.show_dialog) self.cac = QAction(QIcon(I('user_profile.png')), _('Add a custom news source'), self) self.cac.triggered[bool].connect(self.customize_feeds) self.news_menu.addAction(self.cac) self.news_menu.addSeparator() self.all_action = self.news_menu.addAction( _('Download all scheduled news sources'), self.download_all_scheduled) self.timer = QTimer(self) self.timer.start(int(self.INTERVAL * 60 * 1000)) self.timer.timeout.connect(self.check) self.oldest = gconf['oldest_news'] QTimer.singleShot(5 * 1000, self.oldest_check) def database_changed(self, db): self.db = db def oldest_check(self): if self.oldest > 0: delta = timedelta(days=self.oldest) try: ids = list(self.db.tags_older_than(_('News'), delta, must_have_authors=['calibre'])) except: # Happens if library is being switched ids = [] if ids: if ids: self.delete_old_news.emit(ids) QTimer.singleShot(60 * 60 * 1000, self.oldest_check) def show_dialog(self, *args): self.lock.lock() try: d = SchedulerDialog(self.recipe_model) d.download.connect(self.download_clicked) d.exec_() gconf['oldest_news'] = self.oldest = d.old_news.value() d.break_cycles() finally: self.lock.unlock() def customize_feeds(self, *args): from calibre.gui2.dialogs.user_profiles import UserProfiles d = UserProfiles(self._parent, self.recipe_model) try: d.exec_() d.break_cycles() finally: d.deleteLater() def do_download(self, urn): self.lock.lock() try: account_info = self.recipe_model.get_account_info(urn) customize_info = self.recipe_model.get_customize_info(urn) recipe = self.recipe_model.recipe_from_urn(urn) un = pw = None if account_info is not None: un, pw = account_info add_title_tag, custom_tags, keep_issues = customize_info script = self.recipe_model.get_recipe(urn) pt = PersistentTemporaryFile('_builtin.recipe') pt.write(script) pt.close() arg = { 'username': un, 'password': pw, 'add_title_tag':add_title_tag, 'custom_tags':custom_tags, 'recipe':pt.name, 'title':recipe.get('title',''), 'urn':urn, 'keep_issues':keep_issues } self.download_queue.add(urn) self.start_recipe_fetch.emit(arg) finally: self.lock.unlock() def recipe_downloaded(self, arg): self.lock.lock() try: self.recipe_model.update_last_downloaded(arg['urn']) self.download_queue.remove(arg['urn']) finally: self.lock.unlock() def recipe_download_failed(self, arg): self.lock.lock() try: self.recipe_model.update_last_downloaded(arg['urn']) self.download_queue.remove(arg['urn']) finally: self.lock.unlock() def download_clicked(self, urn): if urn is not None: return self.download(urn) for urn in self.recipe_model.scheduled_urns(): if not self.download(urn): break def download_all_scheduled(self): self.download_clicked(None) def has_internet_connection(self): if not internet_connected(): if not self.internet_connection_failed: self.internet_connection_failed = True if self._parent.is_minimized_to_tray: self._parent.status_bar.show_message(self.no_internet_msg, 5000) elif not self.no_internet_dialog.isVisible(): self.no_internet_dialog.show() return False self.internet_connection_failed = False if self.no_internet_dialog.isVisible(): self.no_internet_dialog.hide() return True def download(self, urn): self.lock.lock() if not self.has_internet_connection(): return False doit = urn not in self.download_queue self.lock.unlock() if doit: self.do_download(urn) return True def check(self): recipes = self.recipe_model.get_to_be_downloaded_recipes() for urn in recipes: if not self.download(urn): # No internet connection, we will try again in a minute break
class DeviceBar(QWidget): """ DeviceBar Signals: onDeviceUpdated() onDeviceSelected(str) # str = id onDeviceChanged(str) # str = id """ onDeviceUpdated = pyqtSignal(str, name="onDeviceUpdated") onDeviceSelected = pyqtSignal(str, name="onDeviceSelected") onDeviceChanged = pyqtSignal(str, name="onDeviceChanged") def __init__(self, parent=None, device_type='usb'): super().__init__(parent=parent) # dont show for local if device_type != 'usb': return self.parent = parent self.wait_for_devtype = device_type self.is_waiting = True self._adb = Adb() if not self._adb.min_required: return self._git = Git() self.setAutoFillBackground(True) self.setStyleSheet( 'background-color: crimson; color: white; font-weight: bold; margin: 0; padding: 10px;' ) self.setup() self._timer = QTimer() self._timer.setInterval(500) self._timer.timeout.connect(self._on_timer) self._timer.start() self._timer_step = 0 frida.get_device_manager().on('added', self._on_device) frida.get_device_manager().on('removed', self._on_device) self.devices_thread = DevicesUpdateThread(self) self.devices_thread.onAddDevice.connect(self.on_add_deviceitem) self.devices_thread.onDevicesUpdated.connect(self._on_devices_finished) self._update_thread = FridaUpdateThread(self) self._update_thread._adb = self._adb self._update_thread.onStatusUpdate.connect(self._update_statuslbl) self._update_thread.onFinished.connect(self._frida_updated) self._update_thread.onError.connect(self._on_download_error) self.updated_frida_version = '' self.updated_frida_assets_url = {} self._device_id = None self._devices = [] remote_frida = self._git.get_frida_version() if remote_frida is None: self.updated_frida_version = '' self.updated_frida_assets_url.clear() else: remote_frida = remote_frida[0] self.updated_frida_version = remote_frida['tag_name'] for asset in remote_frida['assets']: try: name = asset['name'] tag_start = name.index('android-') if name.index('server') >= 0: tag = name[tag_start + 8:-3] self.updated_frida_assets_url[tag] = asset[ 'browser_download_url'] except ValueError: pass def setup(self): """ Setup ui """ h_box = QHBoxLayout() h_box.setContentsMargins(0, 0, 0, 0) self.update_label = QLabel('Waiting for Device') self.update_label.setFixedWidth(self.parent.width()) self.update_label.setOpenExternalLinks(True) self.update_label.setTextFormat(Qt.RichText) self.update_label.setFixedHeight(35) self.update_label.setTextInteractionFlags(Qt.TextBrowserInteraction) self._install_btn = QPushButton('Install Frida', self.update_label) self._install_btn.setStyleSheet('padding: 0; border-color: white;') self._install_btn.setGeometry(self.update_label.width() - 110, 5, 100, 25) self._install_btn.clicked.connect(self._on_install_btn) self._install_btn.setVisible(False) self._start_btn = QPushButton('Start Frida', self.update_label) self._start_btn.setStyleSheet('padding: 0; border-color: white;') self._start_btn.setGeometry(self.update_label.width() - 110, 5, 100, 25) self._start_btn.clicked.connect(self._on_start_btn) self._start_btn.setVisible(False) self._update_btn = QPushButton('Update Frida', self.update_label) self._update_btn.setStyleSheet('padding: 0; border-color: white;') self._update_btn.setGeometry(self.update_label.width() - 110, 5, 100, 25) self._update_btn.clicked.connect(self._on_install_btn) self._update_btn.setVisible(False) self._restart_btn = QPushButton('Restart Frida', self.update_label) self._restart_btn.setStyleSheet('padding: 0; border-color: white;') self._restart_btn.setGeometry(self.update_label.width() - 110, 5, 100, 25) self._restart_btn.clicked.connect(self._on_restart_btn) self._restart_btn.setVisible(False) self._devices_combobox = QComboBox(self.update_label) self._devices_combobox.setStyleSheet( 'padding: 2px 5px; border-color: white;') self._devices_combobox.setGeometry(self.update_label.width() - 320, 5, 200, 25) self._devices_combobox.currentIndexChanged.connect( self._on_device_changed) self._devices_combobox.setVisible(False) h_box.addWidget(self.update_label) self.setLayout(h_box) def on_add_deviceitem(self, device_ident): """ Adds an Item to the DeviceComboBox """ if device_ident['type'] == self.wait_for_devtype: if device_ident['name'] not in self._devices: self._devices.append(device_ident) self._timer_step = -1 self.is_waiting = False def _on_device_changed(self, index): device = None device_id = self._devices_combobox.itemData(index) if device_id: try: device = frida.get_device(device_id) except: return if device: self._device_id = device.id self._check_device(device) self.onDeviceChanged.emit(self._device_id) def _check_device(self, frida_device): self.update_label.setStyleSheet('background-color: crimson;') self._install_btn.setVisible(False) self._update_btn.setVisible(False) self._start_btn.setVisible(False) self._restart_btn.setVisible(False) self._adb.device = frida_device.id self._device_id = frida_device.id if self._adb.available(): self.update_label.setText('Device: ' + frida_device.name) # try getting frida version device_frida = self._adb.get_frida_version() # frida not found show install button if device_frida is None: self._install_btn.setVisible(True) else: # frida is old show update button if self.updated_frida_version != device_frida: self._start_btn.setVisible(True) self._update_btn.setVisible(False) # old frida is running allow use of this version if self._adb.is_frida_running(): self._start_btn.setVisible(False) if self.updated_frida_assets_url: self._update_btn.setVisible(True) self.update_label.setStyleSheet( 'background-color: yellowgreen;') self.onDeviceUpdated.emit(frida_device.id) # frida not running show start button elif device_frida and not self._adb.is_frida_running(): self._start_btn.setVisible(True) # frida is running with last version show restart button elif device_frida and self._adb.is_frida_running(): self.update_label.setStyleSheet( 'background-color: yellowgreen;') self._restart_btn.setVisible(True) self.onDeviceUpdated.emit(frida_device.id) elif self._adb.non_root_available(): self.update_label.setText('Device: ' + frida_device.name + ' (NOROOT!)') self.onDeviceUpdated.emit(frida_device.id) def _on_devices_finished(self): if self._devices: if len(self._devices) > 1: self._devices_combobox.clear() self._devices_combobox.setVisible(True) self.update_label.setText('Please select the Device: ') for device in self._devices: self._devices_combobox.addItem(device['name'], device['id']) else: self._devices_combobox.setVisible(False) try: device = frida.get_device(self._devices[0]['id']) self._check_device(device) except: pass def _on_timer(self): if self._timer_step == -1: self._timer.stop() return if self._timer_step == 0: self.update_label.setText(self.update_label.text() + ' .') self._timer_step = 1 elif self._timer_step == 1: self.update_label.setText(self.update_label.text() + '.') self._timer_step = 2 elif self._timer_step == 2: self.update_label.setText(self.update_label.text() + '.') self._timer_step = 3 else: self.update_label.setText( self.update_label.text()[:-self._timer_step]) self._timer_step = 0 if self.is_waiting and self.devices_thread is not None: if not self.devices_thread.isRunning(): self.devices_thread.start() def _on_download_error(self, text): self._timer_step = -1 self.update_label.setStyleSheet('background-color: crimson;') self.update_label.setText(text) self._install_btn.setVisible(True) self._update_btn.setVisible(False) def _on_device(self): self.update_label.setText('Waiting for Device ...') self._timer_step = 3 self.is_waiting = True self._on_timer() def _on_install_btn(self): # urls are empty if not self.updated_frida_assets_url: return arch = self._adb.get_device_arch() request_url = '' if arch is not None and len(arch) > 1: arch = arch.join(arch.split()) if arch == 'arm64' or arch == 'arm64-v8a': request_url = self.updated_frida_assets_url['arm64'] elif arch == 'armeabi-v7a': request_url = self.updated_frida_assets_url['arm'] else: if arch in self.updated_frida_assets_url: request_url = self.updated_frida_assets_url[arch] try: if self._adb.available() and request_url.index( 'https://') == 0: self._install_btn.setVisible(False) self._update_btn.setVisible(False) if self._update_thread is not None: if not self._update_thread.isRunning(): self._update_thread.frida_update_url = request_url self._update_thread.adb = self._adb self._update_thread.start() except ValueError: # something wrong in .git_cache folder print("request_url not set") def _update_statuslbl(self, text): self._timer.stop() self._timer_step = 0 self._timer.start() self.update_label.setText(text) def _frida_updated(self): #self._timer_step = 3 #self.is_waiting = True self._on_devices_finished() def _on_start_btn(self): if self._adb.available(): self._start_btn.setVisible(False) if self._adb.start_frida(): #self.onDeviceUpdated.emit(self._device_id) self._on_devices_finished() else: self._start_btn.setVisible(True) def _on_restart_btn(self): if self._adb.available(): self._restart_btn.setVisible(False) if self._adb.start_frida(restart=True): self._restart_btn.setVisible(True) #self.onDeviceUpdated.emit(self._device_id) self._on_devices_finished()
class TagBrowserWidget(QFrame): # {{{ def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle(QFrame.NoFrame if gprefs['tag_browser_old_look'] else QFrame.StyledPanel) self._parent = parent self._layout = QVBoxLayout(self) self._layout.setContentsMargins(0,0,0,0) # Set up the find box & button self.tb_bar = tbb = TagBrowserBar(self) self.alter_tb, self.item_search, self.search_button = tbb.alter_tb, tbb.item_search, tbb.search_button self.toggle_search_button = tbb.toggle_search_button self._layout.addWidget(tbb) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.lineEdit().returnPressed.connect(self.do_find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[str].connect(self.do_find) # The tags view parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self._layout.insertWidget(0, parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText('<p><b>'+_('No More Matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10,20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect(self.not_found_label_timer_event, type=Qt.QueuedConnection) # The Alter Tag Browser button l = self.alter_tb self.collapse_all_action = ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser collapse all', _('Collapse all'), default_keys=(), action=ac, group=_('Tag browser')) connect_lambda(ac.triggered, self, lambda self: self.tags_view.collapseAll()) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser alter', _('Configure Tag browser'), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(l.showMenu) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate((_('Name'), _('Number of books'), _('Average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip( _('Set the sort order for entries in the Tag browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate((_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc.')) mt.setToolTip(_('All of these category_managers are available by right-clicking ' 'on items in the tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser toggle item', _("'Click' found item"), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.toggle_item) # self.leak_test_timer = QTimer(self) # self.leak_test_timer.timeout.connect(self.test_for_leak) # self.leak_test_timer.start(5000) def save_state(self): gprefs.set('tag browser search box visible', self.toggle_search_button.isChecked()) def toggle_item(self): self.tags_view.toggle_current_index() def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) def find_text_changed(self, str): self.current_find_position = None def set_focus_to_find_box(self): self.tb_bar.set_focus_to_find_box() def do_find(self, str=None): self.current_find_position = None self.find() def find(self): model = self.tags_view.model() model.clear_boxed() txt = unicode(self.item_search.currentText()).strip() if txt.startswith('*'): model.set_categories_filter(txt[1:]) self.tags_view.recount() self.current_find_position = None return if model.get_categories_filter(): model.set_categories_filter(None) self.tags_view.recount() self.current_find_position = None if not txt: return self.item_search.lineEdit().blockSignals(True) self.search_button.setFocus(True) self.item_search.lineEdit().blockSignals(False) key = None colon = txt.rfind(':') if len(txt) > 2 else 0 if colon > 0: key = self._parent.library_view.model().db.\ field_metadata.search_term_to_field_key(txt[:colon]) txt = txt[colon+1:] self.current_find_position = \ model.find_item_node(key, txt, self.current_find_position) if self.current_find_position: self.tags_view.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) if self.tags_view.verticalScrollBar().isVisible(): sbw = self.tags_view.verticalScrollBar().width() else: sbw = 0 width = self.width() - 8 - sbw height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) self.not_found_label.move(4, 10) self.not_found_label_timer.start(2000) def not_found_label_timer_event(self): self.not_found_label.setVisible(False)
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 report_worker_launch_error(self): if parse_worker.launch_error is not None: tb, parse_worker.launch_error = parse_worker.launch_error, None error_dialog( self, _('Failed to launch worker'), _('Failed to launch the worker process used for rendering the preview' ), det_msg=tb, show=True) def show(self, name): if name != self.current_name: self.refresh_timer.stop() self.current_name = name self.report_worker_launch_error() 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 self.report_worker_launch_error() 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 EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): def __init__(self, parent, db, id_to_select, select_sort, select_link): QDialog.__init__(self, parent) Ui_EditAuthorsDialog.__init__(self) self.setupUi(self) # Remove help icon on title bar icon = self.windowIcon() self.setWindowFlags(self.windowFlags() & (~Qt.WindowContextHelpButtonHint)) self.setWindowIcon(icon) try: self.table_column_widths = \ gprefs.get('manage_authors_table_widths', None) geom = gprefs.get('manage_authors_dialog_geometry', bytearray('')) self.restoreGeometry(QByteArray(geom)) except: pass self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) self.buttonBox.accepted.connect(self.accepted) # Set up the column headings self.table.setSelectionMode(QAbstractItemView.SingleSelection) self.table.setColumnCount(3) self.down_arrow_icon = QIcon(I('arrow-down.png')) self.up_arrow_icon = QIcon(I('arrow-up.png')) self.blank_icon = QIcon(I('blank.png')) self.auth_col = QTableWidgetItem(_('Author')) self.table.setHorizontalHeaderItem(0, self.auth_col) self.auth_col.setIcon(self.blank_icon) self.aus_col = QTableWidgetItem(_('Author sort')) self.table.setHorizontalHeaderItem(1, self.aus_col) self.aus_col.setIcon(self.up_arrow_icon) self.aul_col = QTableWidgetItem(_('Link')) self.table.setHorizontalHeaderItem(2, self.aul_col) self.aus_col.setIcon(self.blank_icon) # Add the data self.authors = {} auts = db.get_authors_with_ids() self.table.setRowCount(len(auts)) select_item = None for row, (id, author, sort, link) in enumerate(auts): author = author.replace('|', ',') self.authors[id] = (author, sort, link) aut = tableItem(author) aut.setData(Qt.UserRole, id) sort = tableItem(sort) link = tableItem(link) self.table.setItem(row, 0, aut) self.table.setItem(row, 1, sort) self.table.setItem(row, 2, link) if id_to_select in (id, author): if select_sort: select_item = sort elif select_link: select_item = link else: select_item = aut self.table.resizeColumnsToContents() if self.table.columnWidth(2) < 200: self.table.setColumnWidth(2, 200) # set up the cellChanged signal only after the table is filled self.table.cellChanged.connect(self.cell_changed) # set up sort buttons self.sort_by_author.setCheckable(True) self.sort_by_author.setChecked(False) self.sort_by_author.clicked.connect(self.do_sort_by_author) self.author_order = 1 self.table.sortByColumn(1, Qt.AscendingOrder) self.sort_by_author_sort.clicked.connect(self.do_sort_by_author_sort) self.sort_by_author_sort.setCheckable(True) self.sort_by_author_sort.setChecked(True) self.author_sort_order = 1 self.recalc_author_sort.clicked.connect(self.do_recalc_author_sort) self.auth_sort_to_author.clicked.connect(self.do_auth_sort_to_author) # Position on the desired item if select_item is not None: self.table.setCurrentItem(select_item) self.table.editItem(select_item) self.start_find_pos = select_item.row() * 2 + select_item.column() else: self.table.setCurrentCell(0, 0) self.start_find_pos = -1 # set up the search box self.find_box.initialize('manage_authors_search') self.find_box.lineEdit().returnPressed.connect(self.do_find) self.find_box.editTextChanged.connect(self.find_text_changed) self.find_button.clicked.connect(self.do_find) self.find_button.setDefault(True) l = QLabel(self.table) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText(_('No matches found')) l.setAlignment(Qt.AlignVCenter) l.resize(l.sizeHint()) l.move(10, 20) l.setVisible(False) self.not_found_label.move(40, 40) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) self.table.setContextMenuPolicy(Qt.CustomContextMenu) self.table.customContextMenuRequested.connect(self.show_context_menu) def save_state(self): self.table_column_widths = [] for c in range(0, self.table.columnCount()): self.table_column_widths.append(self.table.columnWidth(c)) gprefs['manage_authors_table_widths'] = self.table_column_widths gprefs['manage_authors_dialog_geometry'] = bytearray( self.saveGeometry()) def resizeEvent(self, *args): QDialog.resizeEvent(self, *args) if self.table_column_widths is not None: for c, w in enumerate(self.table_column_widths): self.table.setColumnWidth(c, w) else: # the vertical scroll bar might not be rendered, so might not yet # have a width. Assume 25. Not a problem because user-changed column # widths will be remembered w = self.table.width() - 25 - self.table.verticalHeader().width() w /= self.table.columnCount() for c in range(0, self.table.columnCount()): self.table.setColumnWidth(c, w) self.save_state() def show_context_menu(self, point): self.context_item = self.table.itemAt(point) case_menu = QMenu(_('Change case')) action_upper_case = case_menu.addAction(_('Upper case')) action_lower_case = case_menu.addAction(_('Lower case')) action_swap_case = case_menu.addAction(_('Swap case')) action_title_case = case_menu.addAction(_('Title case')) action_capitalize = case_menu.addAction(_('Capitalize')) action_upper_case.triggered.connect(self.upper_case) action_lower_case.triggered.connect(self.lower_case) action_swap_case.triggered.connect(self.swap_case) action_title_case.triggered.connect(self.title_case) action_capitalize.triggered.connect(self.capitalize) m = self.au_context_menu = QMenu() ca = m.addAction(_('Copy')) ca.triggered.connect(self.copy_to_clipboard) ca = m.addAction(_('Paste')) ca.triggered.connect(self.paste_from_clipboard) m.addSeparator() if self.context_item is not None and self.context_item.column() == 0: ca = m.addAction(_('Copy to author sort')) ca.triggered.connect(self.copy_au_to_aus) else: ca = m.addAction(_('Copy to author')) ca.triggered.connect(self.copy_aus_to_au) m.addSeparator() m.addMenu(case_menu) m.exec_(self.table.mapToGlobal(point)) def copy_to_clipboard(self): cb = QApplication.clipboard() cb.setText(unicode_type(self.context_item.text())) def paste_from_clipboard(self): cb = QApplication.clipboard() self.context_item.setText(cb.text()) def upper_case(self): self.context_item.setText( icu_upper(unicode_type(self.context_item.text()))) def lower_case(self): self.context_item.setText( icu_lower(unicode_type(self.context_item.text()))) def swap_case(self): self.context_item.setText( unicode_type(self.context_item.text()).swapcase()) def title_case(self): from calibre.utils.titlecase import titlecase self.context_item.setText( titlecase(unicode_type(self.context_item.text()))) def capitalize(self): from calibre.utils.icu import capitalize self.context_item.setText( capitalize(unicode_type(self.context_item.text()))) def copy_aus_to_au(self): row = self.context_item.row() dest = self.table.item(row, 0) dest.setText(self.context_item.text()) def copy_au_to_aus(self): row = self.context_item.row() dest = self.table.item(row, 1) dest.setText(self.context_item.text()) def not_found_label_timer_event(self): self.not_found_label.setVisible(False) def find_text_changed(self): self.start_find_pos = -1 def do_find(self): self.not_found_label.setVisible(False) # For some reason the button box keeps stealing the RETURN shortcut. # Steal it back self.buttonBox.button(QDialogButtonBox.Ok).setDefault(False) self.buttonBox.button(QDialogButtonBox.Ok).setAutoDefault(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(False) self.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False) st = icu_lower(unicode_type(self.find_box.currentText())) for i in range(0, self.table.rowCount() * 2): self.start_find_pos = (self.start_find_pos + 1) % (self.table.rowCount() * 2) r = (self.start_find_pos / 2) % self.table.rowCount() c = self.start_find_pos % 2 item = self.table.item(r, c) text = icu_lower(unicode_type(item.text())) if st in text: self.table.setCurrentItem(item) self.table.setFocus(True) return # Nothing found. Pop up the little dialog for 1.5 seconds self.not_found_label.setVisible(True) self.not_found_label_timer.start(1500) def do_sort_by_author(self): self.author_order = 1 if self.author_order == 0 else 0 self.table.sortByColumn(0, self.author_order) self.sort_by_author.setChecked(True) self.sort_by_author_sort.setChecked(False) self.auth_col.setIcon( self.down_arrow_icon if self.author_order else self.up_arrow_icon) self.aus_col.setIcon(self.blank_icon) def do_sort_by_author_sort(self): self.author_sort_order = 1 if self.author_sort_order == 0 else 0 self.table.sortByColumn(1, self.author_sort_order) self.sort_by_author.setChecked(False) self.sort_by_author_sort.setChecked(True) self.aus_col.setIcon(self.down_arrow_icon if self. author_sort_order else self.up_arrow_icon) self.auth_col.setIcon(self.blank_icon) def accepted(self): self.save_state() self.result = [] for row in range(0, self.table.rowCount()): id = int(self.table.item(row, 0).data(Qt.UserRole)) aut = unicode_type(self.table.item(row, 0).text()).strip() sort = unicode_type(self.table.item(row, 1).text()).strip() link = unicode_type(self.table.item(row, 2).text()).strip() orig_aut, orig_sort, orig_link = self.authors[id] if orig_aut != aut or orig_sort != sort or orig_link != link: self.result.append((id, orig_aut, aut, sort, link)) def do_recalc_author_sort(self): self.table.cellChanged.disconnect() for row in range(0, self.table.rowCount()): item = self.table.item(row, 0) aut = unicode_type(item.text()).strip() c = self.table.item(row, 1) # Sometimes trailing commas are left by changing between copy algs c.setText(author_to_author_sort(aut).rstrip(',')) self.table.setFocus(Qt.OtherFocusReason) self.table.cellChanged.connect(self.cell_changed) def do_auth_sort_to_author(self): self.table.cellChanged.disconnect() for row in range(0, self.table.rowCount()): item = self.table.item(row, 1) aus = unicode_type(item.text()).strip() c = self.table.item(row, 0) # Sometimes trailing commas are left by changing between copy algs c.setText(aus) self.table.setFocus(Qt.OtherFocusReason) self.table.cellChanged.connect(self.cell_changed) def cell_changed(self, row, col): if col == 0: item = self.table.item(row, 0) aut = unicode_type(item.text()).strip() aut_list = string_to_authors(aut) if len(aut_list) != 1: error_dialog( self.parent(), _('Invalid author name'), _('You cannot change an author to multiple authors.') ).exec_() aut = ' % '.join(aut_list) self.table.item(row, 0).setText(aut) c = self.table.item(row, 1) c.setText(author_to_author_sort(aut)) item = c else: item = self.table.item(row, col) self.table.setCurrentItem(item) self.table.scrollToItem(item)
class JobManager(QAbstractTableModel, AdaptSQP): # {{{ job_added = pyqtSignal(int) job_done = pyqtSignal(int) def __init__(self): QAbstractTableModel.__init__(self) SearchQueryParser.__init__(self, ['all']) self.wait_icon = (QIcon(I('jobs.png'))) self.running_icon = (QIcon(I('exec.png'))) self.error_icon = (QIcon(I('dialog_error.png'))) self.done_icon = (QIcon(I('ok.png'))) self.jobs = [] self.add_job = Dispatcher(self._add_job) self.server = Server(limit=int(config['worker_limit']/2.0), enforce_cpu_limit=config['enforce_cpu_limit']) self.threaded_server = ThreadedJobServer() self.changed_queue = Queue() self.timer = QTimer(self) self.timer.timeout.connect(self.update, type=Qt.QueuedConnection) self.timer.start(1000) def columnCount(self, parent=QModelIndex()): return 5 def rowCount(self, parent=QModelIndex()): return len(self.jobs) def headerData(self, section, orientation, role): if role != Qt.DisplayRole: return None if orientation == Qt.Horizontal: return ({ 0: _('Job'), 1: _('Status'), 2: _('Progress'), 3: _('Running time'), 4: _('Start time'), }.get(section, '')) else: return (section+1) def show_tooltip(self, arg): widget, pos = arg QToolTip.showText(pos, self.get_tooltip()) def get_tooltip(self): running_jobs = [j for j in self.jobs if j.run_state == j.RUNNING] waiting_jobs = [j for j in self.jobs if j.run_state == j.WAITING] lines = [ngettext('There is a running job:', 'There are {} running jobs:', len(running_jobs)).format(len(running_jobs))] for job in running_jobs: desc = job.description if not desc: desc = _('Unknown job') p = 100. if job.is_finished else job.percent lines.append('%s: %.0f%% done'%(desc, p)) l = ngettext('There is a waiting job', 'There are {} waiting jobs', len(waiting_jobs)).format(len(waiting_jobs)) lines.extend(['', l]) for job in waiting_jobs: desc = job.description if not desc: desc = _('Unknown job') lines.append(desc) return '\n'.join(['calibre', '']+ lines) def data(self, index, role): try: if role not in (Qt.DisplayRole, Qt.DecorationRole): return None row, col = index.row(), index.column() job = self.jobs[row] if role == Qt.DisplayRole: if col == 0: desc = job.description if not desc: desc = _('Unknown job') return (desc) if col == 1: return (job.status_text) if col == 2: p = 100. if job.is_finished else job.percent return (p) if col == 3: rtime = job.running_time if rtime is None: return None return ('%dm %ds'%(int(rtime)//60, int(rtime)%60)) if col == 4 and job.start_time is not None: return (strftime(u'%H:%M -- %d %b', time.localtime(job.start_time))) if role == Qt.DecorationRole and col == 0: state = job.run_state if state == job.WAITING: return self.wait_icon if state == job.RUNNING: return self.running_icon if job.killed or job.failed: return self.error_icon return self.done_icon except: import traceback traceback.print_exc() return None def update(self): try: self._update() except BaseException: import traceback traceback.print_exc() def _update(self): # Update running time for i, j in enumerate(self.jobs): if j.run_state == j.RUNNING: idx = self.index(i, 3) self.dataChanged.emit(idx, idx) # Update parallel jobs jobs = set([]) while True: try: jobs.add(self.server.changed_jobs_queue.get_nowait()) except Empty: break # Update device jobs while True: try: jobs.add(self.changed_queue.get_nowait()) except Empty: break # Update threaded jobs while True: try: jobs.add(self.threaded_server.changed_jobs.get_nowait()) except Empty: break if jobs: needs_reset = False for job in jobs: orig_state = job.run_state job.update() if orig_state != job.run_state: needs_reset = True if job.is_finished: self.job_done.emit(len(self.unfinished_jobs())) if needs_reset: self.layoutAboutToBeChanged.emit() self.jobs.sort() self.layoutChanged.emit() else: for job in jobs: idx = self.jobs.index(job) self.dataChanged.emit( self.index(idx, 0), self.index(idx, 3)) # Kill parallel jobs that have gone on too long try: wmax_time = gprefs['worker_max_time'] * 60 except: wmax_time = 0 if wmax_time > 0: for job in self.jobs: if isinstance(job, ParallelJob): rtime = job.running_time if (rtime is not None and rtime > wmax_time and job.duration is None): job.timed_out = True self.server.kill_job(job) def _add_job(self, job): self.layoutAboutToBeChanged.emit() self.jobs.append(job) self.jobs.sort() self.job_added.emit(len(self.unfinished_jobs())) self.layoutChanged.emit() def done_jobs(self): return [j for j in self.jobs if j.is_finished] def unfinished_jobs(self): return [j for j in self.jobs if not j.is_finished] def row_to_job(self, row): return self.jobs[row] def has_device_jobs(self, queued_also=False): for job in self.jobs: if isinstance(job, DeviceJob): if job.duration is None: # Running or waiting if (job.is_running or queued_also): return True return False def has_jobs(self): for job in self.jobs: if job.is_running: return True return False def run_job(self, done, name, args=[], kwargs={}, description='', core_usage=1): job = ParallelJob(name, description, done, args=args, kwargs=kwargs) job.core_usage = core_usage self.add_job(job) self.server.add_job(job) return job def run_threaded_job(self, job): self.add_job(job) self.threaded_server.add_job(job) def launch_gui_app(self, name, args=[], kwargs={}, description=''): job = ParallelJob(name, description, lambda x: x, args=args, kwargs=kwargs) self.server.run_job(job, gui=True, redirect_output=False) def _kill_job(self, job): if isinstance(job, ParallelJob): self.server.kill_job(job) elif isinstance(job, ThreadedJob): self.threaded_server.kill_job(job) else: job.kill_on_start = True def hide_jobs(self, rows): for r in rows: self.jobs[r].hidden_in_gui = True for r in rows: self.dataChanged.emit(self.index(r, 0), self.index(r, 0)) def show_hidden_jobs(self): for j in self.jobs: j.hidden_in_gui = False for r in xrange(len(self.jobs)): self.dataChanged.emit(self.index(r, 0), self.index(r, 0)) def kill_job(self, row, view): job = self.jobs[row] if isinstance(job, DeviceJob): return error_dialog(view, _('Cannot kill job'), _('Cannot kill jobs that communicate with the device')).exec_() if job.duration is not None: return error_dialog(view, _('Cannot kill job'), _('Job has already run')).exec_() if not getattr(job, 'killable', True): return error_dialog(view, _('Cannot kill job'), _('This job cannot be stopped'), show=True) self._kill_job(job) def kill_multiple_jobs(self, rows, view): jobs = [self.jobs[row] for row in rows] devjobs = [j for j in jobs if isinstance(j, DeviceJob)] if devjobs: error_dialog(view, _('Cannot kill job'), _('Cannot kill jobs that communicate with the device')).exec_() jobs = [j for j in jobs if not isinstance(j, DeviceJob)] jobs = [j for j in jobs if j.duration is None] unkillable = [j for j in jobs if not getattr(j, 'killable', True)] if unkillable: names = u'\n'.join(as_unicode(j.description) for j in unkillable) error_dialog(view, _('Cannot kill job'), _('Some of the jobs cannot be stopped. Click Show details' ' to see the list of unstoppable jobs.'), det_msg=names, show=True) jobs = [j for j in jobs if getattr(j, 'killable', True)] jobs = [j for j in jobs if j.duration is None] for j in jobs: self._kill_job(j) def kill_all_jobs(self): for job in self.jobs: if (isinstance(job, DeviceJob) or job.duration is not None or not getattr(job, 'killable', True)): continue self._kill_job(job) def terminate_all_jobs(self): self.server.killall() for job in self.jobs: if (isinstance(job, DeviceJob) or job.duration is not None or not getattr(job, 'killable', True)): continue if not isinstance(job, ParallelJob): self._kill_job(job) def universal_set(self): return set([i for i, j in enumerate(self.jobs) if not getattr(j, 'hidden_in_gui', False)]) def get_matches(self, location, query, candidates=None): if candidates is None: candidates = self.universal_set() ans = set() if not query: return ans query = lower(query) for j in candidates: job = self.jobs[j] if job.description and query in lower(job.description): ans.add(j) return ans def find(self, query): query = query.strip() rows = self.parse(query) return rows
class Main( MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin, EbookDownloadMixin): 'The main GUI' proceed_requested = pyqtSignal(object, object) book_converted = pyqtSignal(object, object) def __init__(self, opts, parent=None, gui_debug=None): global _gui MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True) self.jobs_pointer = Pointer(self) self.proceed_requested.connect(self.do_proceed, type=Qt.QueuedConnection) self.proceed_question = ProceedQuestion(self) self.job_error_dialog = JobError(self) self.keyboard = Manager(self) _gui = self self.opts = opts self.device_connected = None self.gui_debug = gui_debug self.iactions = OrderedDict() # Actions for action in interface_actions(): if opts.ignore_plugins and action.plugin_path is not None: continue try: ac = self.init_iaction(action) except: # Ignore errors in loading user supplied plugins import traceback traceback.print_exc() if action.plugin_path is None: raise continue ac.plugin_path = action.plugin_path ac.interface_action_base_plugin = action self.add_iaction(ac) self.load_store_plugins() def init_iaction(self, action): ac = action.load_actual_plugin(self) ac.plugin_path = action.plugin_path ac.interface_action_base_plugin = action action.actual_iaction_plugin_loaded = True return ac def add_iaction(self, ac): acmap = self.iactions if ac.name in acmap: if ac.priority >= acmap[ac.name].priority: acmap[ac.name] = ac else: acmap[ac.name] = ac def load_store_plugins(self): from calibre.gui2.store.loader import Stores self.istores = Stores() for store in available_store_plugins(): if self.opts.ignore_plugins and store.plugin_path is not None: continue try: st = self.init_istore(store) self.add_istore(st) except: # Ignore errors in loading user supplied plugins import traceback traceback.print_exc() if store.plugin_path is None: raise continue self.istores.builtins_loaded() def init_istore(self, store): st = store.load_actual_plugin(self) st.plugin_path = store.plugin_path st.base_plugin = store store.actual_istore_plugin_loaded = True return st def add_istore(self, st): stmap = self.istores if st.name in stmap: if st.priority >= stmap[st.name].priority: stmap[st.name] = st else: stmap[st.name] = st def initialize(self, library_path, db, listener, actions, show_gui=True, splash_screen=None): opts = self.opts self.preferences_action, self.quit_action = actions self.library_path = library_path self.content_server = None self._spare_pool = None self.must_restart_before_config = False self.listener = Listener(listener) self.check_messages_timer = QTimer() self.check_messages_timer.timeout.connect( self.another_instance_wants_to_talk) self.check_messages_timer.start(1000) for ac in self.iactions.values(): try: ac.do_genesis() except Exception: # Ignore errors in third party plugins import traceback traceback.print_exc() if getattr(ac, 'plugin_path', None) is None: raise self.donate_action = QAction(QIcon(I('donate.png')), _('&Donate to support calibre'), self) for st in self.istores.values(): st.do_genesis() MainWindowMixin.init_main_window_mixin(self, db) # Jobs Button {{{ self.job_manager = JobManager() self.jobs_dialog = JobsDialog(self, self.job_manager) self.jobs_button = JobsButton(horizontal=True, parent=self) self.jobs_button.initialize(self.jobs_dialog, self.job_manager) # }}} LayoutMixin.init_layout_mixin(self) DeviceMixin.init_device_mixin(self) self.progress_indicator = ProgressIndicator(self) self.progress_indicator.pos = (0, 20) self.verbose = opts.verbose self.get_metadata = GetMetadata() self.upload_memory = {} self.metadata_dialogs = [] self.default_thumbnail = None self.tb_wrapper = textwrap.TextWrapper(width=40) self.viewers = collections.deque() self.system_tray_icon = None if config['systray_icon']: self.system_tray_icon = factory( app_id='com.calibre-ebook.gui').create_system_tray_icon( parent=self, title='calibre') if self.system_tray_icon is not None: self.system_tray_icon.setIcon(QIcon(I('lt.png'))) self.system_tray_icon.setToolTip(self.jobs_button.tray_tooltip()) self.system_tray_icon.setVisible(True) self.jobs_button.tray_tooltip_updated.connect( self.system_tray_icon.setToolTip) elif config['systray_icon']: prints( 'Failed to create system tray icon, your desktop environment probably does not support the StatusNotifier spec' ) self.system_tray_menu = QMenu(self) self.toggle_to_tray_action = self.system_tray_menu.addAction( QIcon(I('page.png')), '') self.toggle_to_tray_action.triggered.connect( self.system_tray_icon_activated) self.system_tray_menu.addAction(self.donate_action) self.donate_button.setDefaultAction(self.donate_action) self.donate_button.setStatusTip(self.donate_button.toolTip()) self.eject_action = self.system_tray_menu.addAction( QIcon(I('eject.png')), _('&Eject connected device')) self.eject_action.setEnabled(False) self.addAction(self.quit_action) self.system_tray_menu.addAction(self.quit_action) self.keyboard.register_shortcut('quit calibre', _('Quit calibre'), default_keys=('Ctrl+Q', ), action=self.quit_action) if self.system_tray_icon is not None: self.system_tray_icon.setContextMenu(self.system_tray_menu) self.system_tray_icon.activated.connect( self.system_tray_icon_activated) self.quit_action.triggered[bool].connect(self.quit) self.donate_action.triggered[bool].connect(self.donate) self.esc_action = QAction(self) self.addAction(self.esc_action) self.keyboard.register_shortcut('clear current search', _('Clear the current search'), default_keys=('Esc', ), action=self.esc_action) self.esc_action.triggered.connect(self.esc) self.shift_esc_action = QAction(self) self.addAction(self.shift_esc_action) self.keyboard.register_shortcut('focus book list', _('Focus the book list'), default_keys=('Shift+Esc', ), action=self.shift_esc_action) self.shift_esc_action.triggered.connect(self.shift_esc) self.ctrl_esc_action = QAction(self) self.addAction(self.ctrl_esc_action) self.keyboard.register_shortcut('clear virtual library', _('Clear the virtual library'), default_keys=('Ctrl+Esc', ), action=self.ctrl_esc_action) self.ctrl_esc_action.triggered.connect(self.ctrl_esc) self.alt_esc_action = QAction(self) self.addAction(self.alt_esc_action) self.keyboard.register_shortcut('clear additional restriction', _('Clear the additional restriction'), default_keys=('Alt+Esc', ), action=self.alt_esc_action) self.alt_esc_action.triggered.connect( self.clear_additional_restriction) # ###################### Start spare job server ######################## QTimer.singleShot(1000, self.create_spare_pool) # ###################### Location Manager ######################## self.location_manager.location_selected.connect(self.location_selected) self.location_manager.unmount_device.connect( self.device_manager.umount_device) self.location_manager.configure_device.connect( self.configure_connected_device) self.location_manager.update_device_metadata.connect( self.update_metadata_on_device) self.eject_action.triggered.connect(self.device_manager.umount_device) # ################### Update notification ################### UpdateMixin.init_update_mixin(self, opts) # ###################### Search boxes ######################## SearchRestrictionMixin.init_search_restirction_mixin(self) SavedSearchBoxMixin.init_saved_seach_box_mixin(self) # ###################### Library view ######################## LibraryViewMixin.init_library_view_mixin(self, db) SearchBoxMixin.init_search_box_mixin(self) # Requires current_db if show_gui: self.show() if splash_screen is not None: splash_screen.hide() if self.system_tray_icon is not None and self.system_tray_icon.isVisible( ) and opts.start_in_tray: self.hide_windows() self.library_view.model().count_changed_signal.connect( self.iactions['Choose Library'].count_changed) if not gprefs.get('quick_start_guide_added', False): try: add_quick_start_guide(self.library_view, getattr(self, 'db_images', None)) except: import traceback traceback.print_exc() for view in ('library', 'memory', 'card_a', 'card_b'): v = getattr(self, '%s_view' % view) v.selectionModel().selectionChanged.connect(self.update_status_bar) v.model().count_changed_signal.connect(self.update_status_bar) self.library_view.model().count_changed() self.bars_manager.database_changed(self.library_view.model().db) self.library_view.model().database_changed.connect( self.bars_manager.database_changed, type=Qt.QueuedConnection) # ########################## Tags Browser ############################## TagBrowserMixin.init_tag_browser_mixin(self, db) # ######################## Search Restriction ########################## if db.prefs['virtual_lib_on_startup']: self.apply_virtual_library(db.prefs['virtual_lib_on_startup']) self.rebuild_vl_tabs() # ########################## Cover Flow ################################ CoverFlowMixin.init_cover_flow_mixin(self) self._calculated_available_height = min(max_available_height() - 15, self.height()) self.resize(self.width(), self._calculated_available_height) self.build_context_menus() for ac in self.iactions.values(): try: ac.gui_layout_complete() except: import traceback traceback.print_exc() if ac.plugin_path is None: raise if config['autolaunch_server']: self.start_content_server() self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.read_settings() self.finalize_layout() if self.bars_manager.showing_donate: self.donate_button.start_animation() self.set_window_title() for ac in self.iactions.values(): try: ac.initialization_complete() except: import traceback traceback.print_exc() if ac.plugin_path is None: raise self.set_current_library_information(current_library_name(), db.library_id, db.field_metadata) self.keyboard.finalize() self.auto_adder = AutoAdder(gprefs['auto_add_path'], self) self.save_layout_state() # Collect cycles now gc.collect() if show_gui and self.gui_debug is not None: info_dialog( self, _('Debug mode'), '<p>' + _('You have started calibre in debug mode. After you ' 'quit calibre, the debug log will be available in ' 'the file: %s<p>The ' 'log will be displayed automatically.') % self.gui_debug, show=True) self.iactions['Connect Share'].check_smartdevice_menus() QTimer.singleShot(1, self.start_smartdevice) QTimer.singleShot(100, self.update_toggle_to_tray_action) def esc(self, *args): self.clear_button.click() def shift_esc(self): self.current_view().setFocus(Qt.OtherFocusReason) def ctrl_esc(self): self.apply_virtual_library() self.current_view().setFocus(Qt.OtherFocusReason) def start_smartdevice(self): message = None if self.device_manager.get_option('smartdevice', 'autostart'): try: message = self.device_manager.start_plugin('smartdevice') except: message = 'start smartdevice unknown exception' prints(message) import traceback traceback.print_exc() if message: if not self.device_manager.is_running('Wireless Devices'): error_dialog( self, _('Problem starting the wireless device'), _('The wireless device driver had problems starting. ' 'It said "%s"') % message, show=True) self.iactions['Connect Share'].set_smartdevice_action_state() def start_content_server(self, check_started=True): from calibre.library.server.main import start_threaded_server from calibre.library.server import server_config self.content_server = start_threaded_server( self.library_view.model().db, server_config().parse()) self.content_server.state_callback = Dispatcher( self.iactions['Connect Share'].content_server_state_changed) if check_started: self.content_server.start_failure_callback = \ Dispatcher(self.content_server_start_failed) def content_server_start_failed(self, msg): error_dialog(self, _('Failed to start Content Server'), _('Could not start the content server. Error:\n\n%s') % msg, show=True) def resizeEvent(self, ev): MainWindow.resizeEvent(self, ev) self.search.setMaximumWidth(self.width() - 150) def create_spare_pool(self, *args): if self._spare_pool is None: num = min(detect_ncpus(), int(config['worker_limit'] / 2.0)) self._spare_pool = Pool(max_workers=num, name='GUIPool') def spare_pool(self): ans, self._spare_pool = self._spare_pool, None QTimer.singleShot(1000, self.create_spare_pool) return ans def do_proceed(self, func, payload): if callable(func): func(payload) def no_op(self, *args): pass def system_tray_icon_activated(self, r=False): if r in (QSystemTrayIcon.Trigger, QSystemTrayIcon.MiddleClick, False): if self.isVisible(): if self.isMinimized(): self.showNormal() else: self.hide_windows() else: self.show_windows() if self.isMinimized(): self.showNormal() @property def is_minimized_to_tray(self): return getattr(self, '__systray_minimized', False) def ask_a_yes_no_question(self, title, msg, det_msg='', show_copy_button=False, ans_when_user_unavailable=True, skip_dialog_name=None, skipped_value=True): if self.is_minimized_to_tray: return ans_when_user_unavailable return question_dialog(self, title, msg, det_msg=det_msg, show_copy_button=show_copy_button, skip_dialog_name=skip_dialog_name, skip_dialog_skipped_value=skipped_value) def update_toggle_to_tray_action(self, *args): if hasattr(self, 'toggle_to_tray_action'): self.toggle_to_tray_action.setText( _('Hide main window') if self.isVisible( ) else _('Show main window')) def hide_windows(self): for window in QApplication.topLevelWidgets(): if isinstance(window, (MainWindow, QDialog)) and \ window.isVisible(): window.hide() setattr(window, '__systray_minimized', True) self.update_toggle_to_tray_action() def show_windows(self, *args): for window in QApplication.topLevelWidgets(): if getattr(window, '__systray_minimized', False): window.show() setattr(window, '__systray_minimized', False) self.update_toggle_to_tray_action() def test_server(self, *args): if self.content_server is not None and \ self.content_server.exception is not None: error_dialog(self, _('Failed to start content server'), unicode(self.content_server.exception)).exec_() @property def current_db(self): return self.library_view.model().db def another_instance_wants_to_talk(self): try: msg = self.listener.queue.get_nowait() except Empty: return if msg.startswith('launched:'): import json try: argv = json.loads(msg[len('launched:'):]) except ValueError: prints('Failed to decode message from other instance: %r' % msg) if DEBUG: error_dialog( self, 'Invalid message', 'Received an invalid message from other calibre instance.' ' Do you have multiple versions of calibre installed?', det_msg='Invalid msg: %r' % msg, show=True) argv = () if isinstance(argv, (list, tuple)) and len(argv) > 1: files = [ os.path.abspath(p) for p in argv[1:] if not os.path.isdir(p) and os.access(p, os.R_OK) ] if files: self.iactions['Add Books'].add_filesystem_book(files) self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) self.show_windows() self.raise_() self.activateWindow() elif msg.startswith('refreshdb:'): m = self.library_view.model() m.db.new_api.reload_from_db() m.db.data.refresh(clear_caches=False, do_search=False) m.resort() m.research() self.tags_view.recount() elif msg.startswith('shutdown:'): self.quit(confirm_quit=False) elif msg.startswith('bookedited:'): parts = msg.split(':')[1:] try: book_id, fmt, library_id = parts[:3] book_id = int(book_id) m = self.library_view.model() db = m.db.new_api if m.db.library_id == library_id and db.has_id(book_id): db.format_metadata(book_id, fmt, allow_cache=False, update_db=True) db.update_last_modified((book_id, )) m.refresh_ids((book_id, )) except Exception: import traceback traceback.print_exc() else: print msg def current_view(self): '''Convenience method that returns the currently visible view ''' idx = self.stack.currentIndex() if idx == 0: return self.library_view if idx == 1: return self.memory_view if idx == 2: return self.card_a_view if idx == 3: return self.card_b_view def booklists(self): return self.memory_view.model().db, self.card_a_view.model( ).db, self.card_b_view.model().db def library_moved(self, newloc, copy_structure=False, call_close=True, allow_rebuild=False): if newloc is None: return default_prefs = None try: olddb = self.library_view.model().db if copy_structure: default_prefs = olddb.prefs from calibre.utils.formatter_functions import unload_user_template_functions unload_user_template_functions(olddb.library_id) except: olddb = None try: db = LibraryDatabase(newloc, default_prefs=default_prefs) except apsw.Error: if not allow_rebuild: raise import traceback repair = question_dialog( self, _('Corrupted database'), _('The library database at %s appears to be corrupted. Do ' 'you want calibre to try and rebuild it automatically? ' 'The rebuild may not be completely successful.') % force_unicode(newloc, filesystem_encoding), det_msg=traceback.format_exc()) if repair: from calibre.gui2.dialogs.restore_library import repair_library_at if repair_library_at(newloc, parent=self): db = LibraryDatabase(newloc, default_prefs=default_prefs) else: return else: return if self.content_server is not None: self.content_server.set_database(db) self.library_path = newloc prefs['library_path'] = self.library_path self.book_on_device(None, reset=True) db.set_book_on_device_func(self.book_on_device) self.library_view.set_database(db) self.tags_view.set_database(db, self.alter_tb) self.library_view.model().set_book_on_device_func(self.book_on_device) self.status_bar.clear_message() self.search.clear() self.saved_search.clear() self.book_details.reset_info() # self.library_view.model().count_changed() db = self.library_view.model().db self.iactions['Choose Library'].count_changed(db.count()) self.set_window_title() self.apply_named_search_restriction('') # reset restriction to null self.saved_searches_changed( recount=False) # reload the search restrictions combo box if db.prefs['virtual_lib_on_startup']: self.apply_virtual_library(db.prefs['virtual_lib_on_startup']) self.rebuild_vl_tabs() for action in self.iactions.values(): action.library_changed(db) if olddb is not None: try: if call_close: olddb.close() except: import traceback traceback.print_exc() olddb.break_cycles() if self.device_connected: self.set_books_in_library(self.booklists(), reset=True) self.refresh_ondevice() self.memory_view.reset() self.card_a_view.reset() self.card_b_view.reset() self.set_current_library_information(current_library_name(), db.library_id, db.field_metadata) self.library_view.set_current_row(0) # Run a garbage collection now so that it does not freeze the # interface later gc.collect() def set_window_title(self): db = self.current_db restrictions = [ x for x in (db.data.get_base_restriction_name(), db.data.get_search_restriction_name()) if x ] restrictions = ' :: '.join(restrictions) font = QFont() if restrictions: restrictions = ' :: ' + restrictions font.setBold(True) font.setItalic(True) self.virtual_library.setFont(font) title = u'{0} - || {1}{2} ||'.format( __appname__, self.iactions['Choose Library'].library_name(), restrictions) self.setWindowTitle(title) def location_selected(self, location): ''' Called when a location icon is clicked (e.g. Library) ''' page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3 self.stack.setCurrentIndex(page) self.book_details.reset_info() for x in ('tb', 'cb'): splitter = getattr(self, x + '_splitter') splitter.button.setEnabled(location == 'library') for action in self.iactions.values(): action.location_selected(location) if location == 'library': self.virtual_library_menu.setEnabled(True) self.highlight_only_button.setEnabled(True) self.vl_tabs.setEnabled(True) else: self.virtual_library_menu.setEnabled(False) self.highlight_only_button.setEnabled(False) self.vl_tabs.setEnabled(False) # Reset the view in case something changed while it was invisible self.current_view().reset() self.set_number_of_books_shown() self.update_status_bar() def job_exception(self, job, dialog_title=_('Conversion Error')): if not hasattr(self, '_modeless_dialogs'): self._modeless_dialogs = [] minz = self.is_minimized_to_tray if self.isVisible(): for x in list(self._modeless_dialogs): if not x.isVisible(): self._modeless_dialogs.remove(x) try: if 'calibre.ebooks.DRMError' in job.details: if not minz: from calibre.gui2.dialogs.drm_error import DRMErrorMessage d = DRMErrorMessage( self, _('Cannot convert') + ' ' + job.description.split(':')[-1].partition('(')[-1][:-1]) d.setModal(False) d.show() self._modeless_dialogs.append(d) return if 'calibre.ebooks.oeb.transforms.split.SplitError' in job.details: title = job.description.split(':')[-1].partition('(')[-1][:-1] msg = _('<p><b>Failed to convert: %s') % title msg += '<p>' + _(''' Many older ebook reader devices are incapable of displaying EPUB files that have internal components over a certain size. Therefore, when converting to EPUB, calibre automatically tries to split up the EPUB into smaller sized pieces. For some files that are large undifferentiated blocks of text, this splitting fails. <p>You can <b>work around the problem</b> by either increasing the maximum split size under EPUB Output in the conversion dialog, or by turning on Heuristic Processing, also in the conversion dialog. Note that if you make the maximum split size too large, your ebook reader may have trouble with the EPUB. ''') if not minz: d = error_dialog(self, _('Conversion Failed'), msg, det_msg=job.details) d.setModal(False) d.show() self._modeless_dialogs.append(d) return if 'calibre.web.feeds.input.RecipeDisabled' in job.details: if not minz: msg = job.details msg = msg[msg. find('calibre.web.feeds.input.RecipeDisabled:'):] msg = msg.partition(':')[-1] d = error_dialog(self, _('Recipe Disabled'), '<p>%s</p>' % msg) d.setModal(False) d.show() self._modeless_dialogs.append(d) return if 'calibre.ebooks.conversion.ConversionUserFeedBack:' in job.details: if not minz: import json payload = job.details.rpartition( 'calibre.ebooks.conversion.ConversionUserFeedBack:' )[-1] payload = json.loads('{' + payload.partition('{')[-1]) d = { 'info': info_dialog, 'warn': warning_dialog, 'error': error_dialog }.get(payload['level'], error_dialog) d = d(self, payload['title'], '<p>%s</p>' % payload['msg'], det_msg=payload['det_msg']) d.setModal(False) d.show() self._modeless_dialogs.append(d) return except: pass if job.killed: return try: prints(job.details, file=sys.stderr) except: pass if not minz: self.job_error_dialog.show_error(dialog_title, _('<b>Failed</b>') + ': ' + unicode(job.description), det_msg=job.details) def read_settings(self): geometry = config['main_window_geometry'] if geometry is not None: self.restoreGeometry(geometry) self.read_layout_settings() def write_settings(self): with gprefs: # Only write to gprefs once config.set('main_window_geometry', self.saveGeometry()) dynamic.set('sort_history', self.library_view.model().sort_history) self.save_layout_state() def quit(self, checked=True, restart=False, debug_on_restart=False, confirm_quit=True): if confirm_quit and not self.confirm_quit(): return try: self.shutdown() except: pass self.restart_after_quit = restart self.debug_on_restart = debug_on_restart QApplication.instance().quit() def donate(self, *args): open_url(QUrl('http://calibre-ebook.com/donate')) def confirm_quit(self): if self.job_manager.has_jobs(): msg = _('There are active jobs. Are you sure you want to quit?') if self.job_manager.has_device_jobs(): msg = '<p>'+__appname__ + \ _(''' is communicating with the device!<br> Quitting may cause corruption on the device.<br> Are you sure you want to quit?''')+'</p>' if not question_dialog(self, _('Active jobs'), msg): return False from calibre.db.delete_service import has_jobs if has_jobs(): msg = _('Some deleted books are still being moved to the Recycle ' 'Bin, if you quit now, they will be left behind. Are you ' 'sure you want to quit?') if not question_dialog(self, _('Active jobs'), msg): return False return True def shutdown(self, write_settings=True): self.grid_view.shutdown() try: db = self.library_view.model().db cf = db.clean except: pass else: cf() # Save the current field_metadata for applications like calibre2opds # Goes here, because if cf is valid, db is valid. db.prefs['field_metadata'] = db.field_metadata.all_metadata() db.commit_dirty_cache() db.prefs.write_serialized(prefs['library_path']) for action in self.iactions.values(): if not action.shutting_down(): return if write_settings: self.write_settings() self.check_messages_timer.stop() if hasattr(self, 'update_checker'): self.update_checker.shutdown() self.listener.close() self.job_manager.server.close() self.job_manager.threaded_server.close() self.device_manager.keep_going = False self.auto_adder.stop() mb = self.library_view.model().metadata_backup if mb is not None: mb.stop() self.hide_windows() try: try: if self.content_server is not None: s = self.content_server self.content_server = None s.exit() except: pass except KeyboardInterrupt: pass if self._spare_pool is not None: self._spare_pool.shutdown() from calibre.db.delete_service import shutdown shutdown() time.sleep(2) self.istores.join() self.hide_windows() # Do not report any errors that happen after the shutdown sys.excepthook = sys.__excepthook__ return True def run_wizard(self, *args): if self.confirm_quit(): self.run_wizard_b4_shutdown = True self.restart_after_quit = True try: self.shutdown(write_settings=False) except: pass QApplication.instance().quit() def closeEvent(self, e): self.write_settings() if self.system_tray_icon is not None and self.system_tray_icon.isVisible( ): if not dynamic['systray_msg'] and not isosx: info_dialog( self, 'calibre', 'calibre ' + _('will keep running in the system tray. To close it, ' 'choose <b>Quit</b> in the context menu of the ' 'system tray.'), show_copy_button=False).exec_() dynamic['systray_msg'] = True self.hide_windows() e.ignore() else: if self.confirm_quit(): try: self.shutdown(write_settings=False) except: import traceback traceback.print_exc() e.accept() else: e.ignore()
ui.plainTextEdit.setFont(font) window.show() tn = telnetlib.Telnet('arda.pp.ru', 4000) tn.set_debuglevel(100) def telnet(): while True: data = tn.read_some() if data: data = data.replace('\n\r', '\n') ui.plainTextEdit.insertPlainText(data.decode('cp1251')) gevent.sleep(0) telnet = gevent.spawn(telnet) def gevent_pull(): gevent.sleep(0) timer = QTimer() timer.timeout.connect(gevent_pull) timer.start(0) result = app.exec_() telnet.kill() sys.exit(result)
class ScudCloud(QtWidgets.QMainWindow): forceClose = False messages = 0 speller = Speller() title = 'ScudCloud' def __init__(self, debug = False, minimized = None, urgent_hint = None, settings_path = '', cache_path = ''): super(ScudCloud, self).__init__(None) self.debug = debug self.minimized = minimized self.urgent_hint = urgent_hint self.setWindowTitle(self.title) self.settings_path = settings_path self.cache_path = cache_path self.notifier = Notifier(Resources.APP_NAME, Resources.get_path('scudcloud.png')) self.settings = QSettings(self.settings_path + '/scudcloud_qt5.cfg', QSettings.IniFormat) self.notifier.enabled = self.settings.value('Notifications', defaultValue=True, type=bool) self.identifier = self.settings.value("Domain") if Unity is not None: self.launcher = Unity.LauncherEntry.get_for_desktop_id("scudcloud.desktop") else: self.launcher = DummyLauncher(self) self.webSettings() self.snippetsSettings() self.leftPane = LeftPane(self) self.stackedWidget = QtWidgets.QStackedWidget() centralWidget = QtWidgets.QWidget(self) layout = QtWidgets.QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.leftPane) layout.addWidget(self.stackedWidget) centralWidget.setLayout(layout) self.setCentralWidget(centralWidget) self.startURL = Resources.SIGNIN_URL if self.identifier is not None: if isinstance(self.identifier, str): self.domains = self.identifier.split(",") else: self.domains = self.identifier self.startURL = self.normalize(self.domains[0]) else: self.domains = [] self.addWrapper(self.startURL) self.addMenu() self.tray = Systray(self) self.systray(self.minimized) self.installEventFilter(self) self.statusBar().showMessage('Loading Slack...') self.tickler = QTimer(self) self.tickler.setInterval(1800000) # Watch for ScreenLock events if DBusQtMainLoop is not None: DBusQtMainLoop(set_as_default=True) sessionBus = dbus.SessionBus() # Ubuntu 12.04 and other distros sessionBus.add_match_string("type='signal',interface='org.gnome.ScreenSaver'") # Ubuntu 14.04 sessionBus.add_match_string("type='signal',interface='com.ubuntu.Upstart0_6'") # Ubuntu 16.04 and KDE sessionBus.add_match_string("type='signal',interface='org.freedesktop.ScreenSaver'") # Cinnamon sessionBus.add_match_string("type='signal',interface='org.cinnamon.ScreenSaver'") sessionBus.add_message_filter(self.screenListener) self.tickler.timeout.connect(self.sendTickle) # If dbus is not present, tickler timer will act like a blocker to not send tickle too often else: self.tickler.setSingleShot(True) self.tickler.start() def screenListener(self, bus, message): event = message.get_member() # "ActiveChanged" for Ubuntu 12.04 and other distros. "EventEmitted" for Ubuntu 14.04 and above if event == "ActiveChanged" or event == "EventEmitted": arg = message.get_args_list()[0] # True for Ubuntu 12.04 and other distros. "desktop-lock" for Ubuntu 14.04 and above if (arg == True or arg == "desktop-lock") and self.tickler.isActive(): self.tickler.stop() elif (arg == False or arg == "desktop-unlock") and not self.tickler.isActive(): self.sendTickle() self.tickler.start() def sendTickle(self): for i in range(0, self.stackedWidget.count()): self.stackedWidget.widget(i).sendTickle() def addWrapper(self, url): webView = Wrapper(self) webView.load(QtCore.QUrl(url)) webView.show() webView.setZoomFactor(self.zoom) self.stackedWidget.addWidget(webView) self.stackedWidget.setCurrentWidget(webView) self.clearMemory() def webSettings(self): self.cookiesjar = PersistentCookieJar(self) self.zoom = self.readZoom() # We don't want Flash (it causes a lot of trouble in some distros) QWebSettings.globalSettings().setAttribute(QWebSettings.PluginsEnabled, False) # We don't need Java QWebSettings.globalSettings().setAttribute(QWebSettings.JavaEnabled, False) # Enabling Local Storage (now required by Slack) QWebSettings.globalSettings().setAttribute(QWebSettings.LocalStorageEnabled, True) # We need browsing history (required to not limit LocalStorage) QWebSettings.globalSettings().setAttribute(QWebSettings.PrivateBrowsingEnabled, False) # Enabling Cache self.diskCache = QNetworkDiskCache(self) self.diskCache.setCacheDirectory(self.cache_path) # Required for copy and paste clipboard integration QWebSettings.globalSettings().setAttribute(QWebSettings.JavascriptCanAccessClipboard, True) # Enabling Inspeclet only when --debug=True (requires more CPU usage) QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, self.debug) # Sharing the same networkAccessManager self.networkAccessManager = QNetworkAccessManager(self) self.networkAccessManager.setCookieJar(self.cookiesjar) self.networkAccessManager.setCache(self.diskCache) def snippetsSettings(self): self.disable_snippets = self.settings.value("Snippets") if self.disable_snippets is not None: self.disable_snippets = self.disable_snippets == "False" else: self.disable_snippets = False if self.disable_snippets: disable_snippets_css = '' with open(Resources.get_path('disable_snippets.css'), 'r') as f: disable_snippets_css = f.read() with open(os.path.join(self.cache_path, 'resources.css'), 'a') as f: f.write(disable_snippets_css) def toggleFullScreen(self): if self.isFullScreen(): self.showMaximized() else: self.showFullScreen() def toggleMenuBar(self): menu = self.menuBar() state = menu.isHidden() menu.setVisible(state) if state: self.settings.setValue("Menu", "True") else: self.settings.setValue("Menu", "False") def restore(self): geometry = self.settings.value("geometry") if geometry is not None: self.restoreGeometry(geometry) windowState = self.settings.value("windowState") if windowState is not None: self.restoreState(windowState) else: self.setWindowState(QtCore.Qt.WindowMaximized) def systray(self, show=None): if show is None: show = self.settings.value("Systray") == "True" if show: self.tray.show() self.menus["file"]["close"].setEnabled(True) self.settings.setValue("Systray", "True") else: self.tray.setVisible(False) self.menus["file"]["close"].setEnabled(False) self.settings.setValue("Systray", "False") def readZoom(self): default = 1 if self.settings.value("Zoom") is not None: default = float(self.settings.value("Zoom")) return default def setZoom(self, factor=1): if factor > 0: for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) widget.setZoomFactor(factor) self.settings.setValue("Zoom", factor) def zoomIn(self): self.setZoom(self.current().zoomFactor() + 0.1) def zoomOut(self): self.setZoom(self.current().zoomFactor() - 0.1) def zoomReset(self): self.setZoom() def addTeam(self): self.switchTo(Resources.SIGNIN_URL) def addMenu(self): # We'll register the webpage shorcuts with the window too (Fixes #338) undo = self.current().pageAction(QWebPage.Undo) redo = self.current().pageAction(QWebPage.Redo) cut = self.current().pageAction(QWebPage.Cut) copy = self.current().pageAction(QWebPage.Copy) paste = self.current().pageAction(QWebPage.Paste) back = self.current().pageAction(QWebPage.Back) forward = self.current().pageAction(QWebPage.Forward) reload = self.current().pageAction(QWebPage.Reload) self.menus = { "file": { "preferences": self.createAction("Preferences", lambda : self.current().preferences()), "systray": self.createAction("Close to Tray", self.systray, None, True), "addTeam": self.createAction("Sign in to Another Team", lambda : self.addTeam()), "signout": self.createAction("Signout", lambda : self.current().logout()), "close": self.createAction("Close", self.close, QKeySequence.Close), "exit": self.createAction("Quit", self.exit, QKeySequence.Quit) }, "edit": { "undo": self.createAction(undo.text(), lambda : self.current().page().triggerAction(QWebPage.Undo), undo.shortcut()), "redo": self.createAction(redo.text(), lambda : self.current().page().triggerAction(QWebPage.Redo), redo.shortcut()), "cut": self.createAction(cut.text(), lambda : self.current().page().triggerAction(QWebPage.Cut), cut.shortcut()), "copy": self.createAction(copy.text(), lambda : self.current().page().triggerAction(QWebPage.Copy), copy.shortcut()), "paste": self.createAction(paste.text(), lambda : self.current().page().triggerAction(QWebPage.Paste), paste.shortcut()), "back": self.createAction(back.text(), lambda : self.current().page().triggerAction(QWebPage.Back), back.shortcut()), "forward": self.createAction(forward.text(), lambda : self.current().page().triggerAction(QWebPage.Forward), forward.shortcut()), "reload": self.createAction(reload.text(), lambda : self.current().page().triggerAction(QWebPage.Reload), reload.shortcut()), }, "view": { "zoomin": self.createAction("Zoom In", self.zoomIn, QKeySequence.ZoomIn), "zoomout": self.createAction("Zoom Out", self.zoomOut, QKeySequence.ZoomOut), "reset": self.createAction("Reset", self.zoomReset, QtCore.Qt.CTRL + QtCore.Qt.Key_0), "fullscreen": self.createAction("Toggle Full Screen", self.toggleFullScreen, QtCore.Qt.Key_F11), "hidemenu": self.createAction("Toggle Menubar", self.toggleMenuBar, QtCore.Qt.Key_F12) }, "help": { "help": self.createAction("Help and Feedback", lambda : self.current().help(), QKeySequence.HelpContents), "center": self.createAction("Slack Help Center", lambda : self.current().helpCenter()), "about": self.createAction("About", lambda : self.current().about()) } } menu = self.menuBar() fileMenu = menu.addMenu("&File") fileMenu.addAction(self.menus["file"]["preferences"]) fileMenu.addAction(self.menus["file"]["systray"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["addTeam"]) fileMenu.addAction(self.menus["file"]["signout"]) fileMenu.addSeparator() fileMenu.addAction(self.menus["file"]["close"]) fileMenu.addAction(self.menus["file"]["exit"]) editMenu = menu.addMenu("&Edit") editMenu.addAction(self.menus["edit"]["undo"]) editMenu.addAction(self.menus["edit"]["redo"]) editMenu.addSeparator() editMenu.addAction(self.menus["edit"]["cut"]) editMenu.addAction(self.menus["edit"]["copy"]) editMenu.addAction(self.menus["edit"]["paste"]) editMenu.addSeparator() editMenu.addAction(self.menus["edit"]["back"]) editMenu.addAction(self.menus["edit"]["forward"]) editMenu.addAction(self.menus["edit"]["reload"]) viewMenu = menu.addMenu("&View") viewMenu.addAction(self.menus["view"]["zoomin"]) viewMenu.addAction(self.menus["view"]["zoomout"]) viewMenu.addAction(self.menus["view"]["reset"]) viewMenu.addSeparator() viewMenu.addAction(self.menus["view"]["fullscreen"]) viewMenu.addAction(self.menus["view"]["hidemenu"]) helpMenu = menu.addMenu("&Help") helpMenu.addAction(self.menus["help"]["help"]) helpMenu.addAction(self.menus["help"]["center"]) helpMenu.addSeparator() helpMenu.addAction(self.menus["help"]["about"]) self.enableMenus(False) showSystray = self.settings.value("Systray") == "True" self.menus["file"]["systray"].setChecked(showSystray) self.menus["file"]["close"].setEnabled(showSystray) # Restore menu visibility visible = self.settings.value("Menu") if visible is not None and visible == "False": menu.setVisible(False) def enableMenus(self, enabled): self.menus["file"]["preferences"].setEnabled(enabled == True) self.menus["file"]["addTeam"].setEnabled(enabled == True) self.menus["file"]["signout"].setEnabled(enabled == True) self.menus["help"]["help"].setEnabled(enabled == True) def createAction(self, text, slot, shortcut=None, checkable=False): action = QtWidgets.QAction(text, self) action.triggered.connect(slot) if shortcut is not None: action.setShortcut(shortcut) self.addAction(action) if checkable: action.setCheckable(True) return action def normalize(self, url): if url.endswith(".slack.com"): url+= "/" elif not url.endswith(".slack.com/"): url = "https://"+url+".slack.com/" return url def current(self): return self.stackedWidget.currentWidget() def teams(self, teams): if len(self.domains) == 0: self.domains.append(teams[0]['team_url']) team_list = [t['team_url'] for t in teams] for t in teams: for i in range(0, len(self.domains)): self.domains[i] = self.normalize(self.domains[i]) # When team_icon is missing, the team already exists (Fixes #381, #391) if 'team_icon' in t: if self.domains[i] in team_list: add = next(item for item in teams if item['team_url'] == self.domains[i]) if 'team_icon' in add: self.leftPane.addTeam(add['id'], add['team_name'], add['team_url'], add['team_icon']['image_44'], add == teams[0]) # Adding new teams and saving loading positions if t['team_url'] not in self.domains: self.leftPane.addTeam(t['id'], t['team_name'], t['team_url'], t['team_icon']['image_44'], t == teams[0]) self.domains.append(t['team_url']) self.settings.setValue("Domain", self.domains) if len(teams) > 1: self.leftPane.show() def switchTo(self, url): exists = False for i in range(0, self.stackedWidget.count()): if self.stackedWidget.widget(i).url().toString().startswith(url): self.stackedWidget.setCurrentIndex(i) self.quicklist(self.current().listChannels()) self.current().setFocus() self.leftPane.click(i) self.clearMemory() exists = True break if not exists: self.addWrapper(url) def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.ActivationChange and self.isActiveWindow(): self.focusInEvent(event) if event.type() == QtCore.QEvent.KeyPress: # Ctrl + <n> modifiers = QtWidgets.QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ControlModifier: if event.key() == QtCore.Qt.Key_1: self.leftPane.click(0) elif event.key() == QtCore.Qt.Key_2: self.leftPane.click(1) elif event.key() == QtCore.Qt.Key_3: self.leftPane.click(2) elif event.key() == QtCore.Qt.Key_4: self.leftPane.click(3) elif event.key() == QtCore.Qt.Key_5: self.leftPane.click(4) elif event.key() == QtCore.Qt.Key_6: self.leftPane.click(5) elif event.key() == QtCore.Qt.Key_7: self.leftPane.click(6) elif event.key() == QtCore.Qt.Key_8: self.leftPane.click(7) elif event.key() == QtCore.Qt.Key_9: self.leftPane.click(8) # Ctrl + Tab elif event.key() == QtCore.Qt.Key_Tab: self.leftPane.clickNext(1) # Ctrl + BackTab if (modifiers & QtCore.Qt.ControlModifier) and (modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_Backtab: self.leftPane.clickNext(-1) # Ctrl + Shift + <key> if (modifiers & QtCore.Qt.ShiftModifier) and (modifiers & QtCore.Qt.ShiftModifier): if event.key() == QtCore.Qt.Key_V: self.current().createSnippet() return QtWidgets.QMainWindow.eventFilter(self, obj, event); def focusInEvent(self, event): self.launcher.set_property("urgent", False) self.tray.stopAlert() # Let's tickle all teams on window focus, but only if tickle was not fired in last 30 minutes if DBusQtMainLoop is None and not self.tickler.isActive(): self.sendTickle() self.tickler.start() def titleChanged(self): self.setWindowTitle(self.current().title()) def setForceClose(self): self.forceClose = True def closeEvent(self, event): if not self.forceClose and self.settings.value("Systray") == "True": self.hide() event.ignore() elif self.forceClose: self.cookiesjar.save() self.settings.setValue("Domain", self.domains) self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) self.settings.setValue("Domain", self.domains) def show(self): self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) self.activateWindow() self.setVisible(True) def exit(self): # Make sure tray is not visible (Fixes #513) self.tray.setVisible(False) self.setForceClose() self.close() def quicklist(self, channels): if Dbusmenu is not None: if channels is not None: ql = Dbusmenu.Menuitem.new() self.launcher.set_property("quicklist", ql) for c in channels: if type(c) is dict and hasattr(c, '__getitem__') and c['is_member']: item = Dbusmenu.Menuitem.new () item.property_set (Dbusmenu.MENUITEM_PROP_LABEL, "#"+c['name']) item.property_set ("id", c['name']) item.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, True) item.connect(Dbusmenu.MENUITEM_SIGNAL_ITEM_ACTIVATED, self.current().openChannel) ql.child_append(item) self.launcher.set_property("quicklist", ql) def notify(self, title, message, icon): if self.debug: print("Notification: title [{}] message [{}] icon [{}]".format(title, message, icon)) self.notifier.notify(title, message, icon) self.alert() def alert(self): if not self.isActiveWindow(): self.launcher.set_property("urgent", True) self.tray.alert() if self.urgent_hint is True: QApplication.alert(self) def count(self): total = 0 unreads = 0 for i in range(0, self.stackedWidget.count()): widget = self.stackedWidget.widget(i) highlights = widget.highlights unreads+= widget.unreads total+=highlights if total > self.messages: self.alert() if 0 == total: self.launcher.set_property("count_visible", False) self.tray.setCounter(0) if unreads > 0: self.setWindowTitle("*{}".format(self.title)) else: self.setWindowTitle(self.title) else: self.tray.setCounter(total) self.launcher.set_property("count", total) self.launcher.set_property("count_visible", True) self.setWindowTitle("[{}]{}".format(str(total), self.title)) self.messages = total def clearMemory(self): QWebSettings.globalSettings().clearMemoryCaches() QWebSettings.globalSettings().clearIconDatabase()
class MaterialManager(QObject): materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated. favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed def __init__(self, container_registry, parent = None): super().__init__(parent) self._application = Application.getInstance() self._container_registry = container_registry # type: ContainerRegistry # Material_type -> generic material metadata self._fallback_materials_map = dict() # type: Dict[str, Dict[str, Any]] # Root_material_id -> MaterialGroup self._material_group_map = dict() # type: Dict[str, MaterialGroup] # Approximate diameter str self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]] # We're using these two maps to convert between the specific diameter material id and the generic material id # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant # i.e. generic_pla -> generic_pla_175 # root_material_id -> approximate diameter str -> root_material_id for that diameter self._material_diameter_map = defaultdict(dict) # type: Dict[str, Dict[str, str]] # Material id including diameter (generic_pla_175) -> material root id (generic_pla) self._diameter_material_map = dict() # type: Dict[str, str] # This is used in Legacy UM3 send material function and the material management page. # GUID -> a list of material_groups self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]] # The machine definition ID for the non-machine-specific materials. # This is used as the last fallback option if the given machine-specific material(s) cannot be found. self._default_machine_definition_id = "fdmprinter" self._default_approximate_diameter_for_quality_search = "3" # When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't # want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't # react too many time. self._update_timer = QTimer(self) self._update_timer.setInterval(300) self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self._updateMaps) self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged) self._container_registry.containerAdded.connect(self._onContainerMetadataChanged) self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged) self._favorites = set() # type: Set[str] def initialize(self) -> None: # Find all materials and put them in a matrix for quick search. material_metadatas = {metadata["id"]: metadata for metadata in self._container_registry.findContainersMetadata(type = "material") if metadata.get("GUID")} # type: Dict[str, Dict[str, Any]] self._material_group_map = dict() # type: Dict[str, MaterialGroup] # Map #1 # root_material_id -> MaterialGroup for material_id, material_metadata in material_metadatas.items(): # We don't store empty material in the lookup tables if material_id == "empty_material": continue root_material_id = material_metadata.get("base_file", "") if root_material_id not in self._material_group_map: self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id])) self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id) group = self._material_group_map[root_material_id] # Store this material in the group of the appropriate root material. if material_id != root_material_id: new_node = MaterialNode(material_metadata) group.derived_material_node_list.append(new_node) # Order this map alphabetically so it's easier to navigate in a debugger self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0])) # Map #1.5 # GUID -> material group list self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]] for root_material_id, material_group in self._material_group_map.items(): guid = material_group.root_material_node.getMetaDataEntry("GUID", "") self._guid_material_groups_map[guid].append(material_group) # Map #2 # Lookup table for material type -> fallback material metadata, only for read-only materials grouped_by_type_dict = dict() # type: Dict[str, Any] material_types_without_fallback = set() for root_material_id, material_node in self._material_group_map.items(): material_type = material_node.root_material_node.getMetaDataEntry("material", "") if material_type not in grouped_by_type_dict: grouped_by_type_dict[material_type] = {"generic": None, "others": []} material_types_without_fallback.add(material_type) brand = material_node.root_material_node.getMetaDataEntry("brand", "") if brand.lower() == "generic": to_add = True if material_type in grouped_by_type_dict: diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "") if diameter != self._default_approximate_diameter_for_quality_search: to_add = False # don't add if it's not the default diameter if to_add: # Checking this first allow us to differentiate between not read only materials: # - if it's in the list, it means that is a new material without fallback # - if it is not, then it is a custom material with a fallback material (parent) if material_type in material_types_without_fallback: grouped_by_type_dict[material_type] = material_node.root_material_node._metadata material_types_without_fallback.remove(material_type) # Remove the materials that have no fallback materials for material_type in material_types_without_fallback: del grouped_by_type_dict[material_type] self._fallback_materials_map = grouped_by_type_dict # Map #3 # There can be multiple material profiles for the same material with different diameters, such as "generic_pla" # and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can # be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID # for quality search. self._material_diameter_map = defaultdict(dict) self._diameter_material_map = dict() # Group the material IDs by the same name, material, brand, and color but with different diameters. material_group_dict = dict() # type: Dict[Tuple[Any], Dict[str, str]] keys_to_fetch = ("name", "material", "brand", "color") for root_material_id, machine_node in self._material_group_map.items(): root_material_metadata = machine_node.root_material_node._metadata key_data_list = [] # type: List[Any] for key in keys_to_fetch: key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key)) key_data = cast(Tuple[Any], tuple(key_data_list)) # type: Tuple[Any] # If the key_data doesn't exist, it doesn't matter if the material is read only... if key_data not in material_group_dict: material_group_dict[key_data] = dict() else: # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it if not machine_node.is_read_only: continue approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "") material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "") # Map [root_material_id][diameter] -> root_material_id for this diameter for data_dict in material_group_dict.values(): for root_material_id1 in data_dict.values(): if root_material_id1 in self._material_diameter_map: continue diameter_map = data_dict for root_material_id2 in data_dict.values(): self._material_diameter_map[root_material_id2] = diameter_map default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search) if default_root_material_id is None: default_root_material_id = list(data_dict.values())[0] # no default diameter present, just take "the" only one for root_material_id in data_dict.values(): self._diameter_material_map[root_material_id] = default_root_material_id # Map #4 # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]] for material_metadata in material_metadatas.values(): self.__addMaterialMetadataIntoLookupTree(material_metadata) favorites = self._application.getPreferences().getValue("cura/favorite_materials") for item in favorites.split(";"): self._favorites.add(item) self.materialsUpdated.emit() def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None: material_id = material_metadata["id"] # We don't store empty material in the lookup tables if material_id == "empty_material": return root_material_id = material_metadata["base_file"] definition = material_metadata["definition"] approximate_diameter = material_metadata["approximate_diameter"] if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map: self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {} machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[ approximate_diameter] if definition not in machine_nozzle_buildplate_material_map: machine_nozzle_buildplate_material_map[definition] = MaterialNode() # This is a list of information regarding the intermediate nodes: # nozzle -> buildplate nozzle_name = material_metadata.get("variant_name") buildplate_name = material_metadata.get("buildplate_name") intermediate_node_info_list = [(nozzle_name, VariantType.NOZZLE), (buildplate_name, VariantType.BUILD_PLATE), ] variant_manager = self._application.getVariantManager() machine_node = machine_nozzle_buildplate_material_map[definition] current_node = machine_node current_intermediate_node_info_idx = 0 error_message = None # type: Optional[str] while current_intermediate_node_info_idx < len(intermediate_node_info_list): variant_name, variant_type = intermediate_node_info_list[current_intermediate_node_info_idx] if variant_name is not None: # The new material has a specific variant, so it needs to be added to that specific branch in the tree. variant = variant_manager.getVariantNode(definition, variant_name, variant_type) if variant is None: error_message = "Material {id} contains a variant {name} that does not exist.".format( id = material_metadata["id"], name = variant_name) break # Update the current node to advance to a more specific branch if variant_name not in current_node.children_map: current_node.children_map[variant_name] = MaterialNode() current_node = current_node.children_map[variant_name] current_intermediate_node_info_idx += 1 if error_message is not None: Logger.log("e", "%s It will not be added into the material lookup tree.", error_message) self._container_registry.addWrongContainerId(material_metadata["id"]) return # Add the material to the current tree node, which is the deepest (the most specific) branch we can find. # Sanity check: Make sure that there is no duplicated materials. if root_material_id in current_node.material_map: Logger.log("e", "Duplicated material [%s] with root ID [%s]. It has already been added.", material_id, root_material_id) ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id) return current_node.material_map[root_material_id] = MaterialNode(material_metadata) def _updateMaps(self): Logger.log("i", "Updating material lookup data ...") self.initialize() def _onContainerMetadataChanged(self, container): self._onContainerChanged(container) def _onContainerChanged(self, container): container_type = container.getMetaDataEntry("type") if container_type != "material": return # update the maps self._update_timer.start() def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]: return self._material_group_map.get(root_material_id) def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str: return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id) def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: return self._diameter_material_map.get(root_material_id, "") def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]: return self._guid_material_groups_map.get(guid) # # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str], buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter) return dict() machine_definition_id = machine_definition.getId() # If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id) nozzle_node = None buildplate_node = None if nozzle_name is not None and machine_node is not None: nozzle_node = machine_node.getChildNode(nozzle_name) # Get buildplate node if possible if nozzle_node is not None and buildplate_name is not None: buildplate_node = nozzle_node.getChildNode(buildplate_name) nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node] # Fallback mechanism of finding materials: # 1. buildplate-specific material # 2. nozzle-specific material # 3. machine-specific material # 4. generic material (for fdmprinter) machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", []) material_id_metadata_dict = dict() # type: Dict[str, MaterialNode] excluded_materials = set() for current_node in nodes_to_check: if current_node is None: continue # Only exclude the materials that are explicitly specified in the "exclude_materials" field. # Do not exclude other materials that are of the same type. for material_id, node in current_node.material_map.items(): if material_id in machine_exclude_materials: excluded_materials.add(material_id) continue if material_id not in material_id_metadata_dict: material_id_metadata_dict[material_id] = node if excluded_materials: Logger.log("d", "Exclude materials {excluded_materials} for machine {machine_definition_id}".format(excluded_materials = ", ".join(excluded_materials), machine_definition_id = machine_definition_id)) return material_id_metadata_dict # # A convenience function to get available materials for the given machine with the extruder position. # def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack", extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]: buildplate_name = machine.getBuildplateName() nozzle_name = None if extruder_stack.variant.getId() != "empty_variant": nozzle_name = extruder_stack.variant.getName() diameter = extruder_stack.getApproximateMaterialDiameter() # Fetch the available materials (ContainerNode) for the current active machine and extruder setup. return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter) # # Gets MaterialNode for the given extruder and machine with the given material name. # Returns None if: # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str], buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]", diameter, rounded_diameter, root_material_id) return None # If there are nozzle materials, get the nozzle-specific material machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] # type: Dict[str, MaterialNode] machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) nozzle_node = None buildplate_node = None # Fallback for "fdmprinter" if the machine-specific materials cannot be found if machine_node is None: machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id) if machine_node is not None and nozzle_name is not None: nozzle_node = machine_node.getChildNode(nozzle_name) if nozzle_node is not None and buildplate_name is not None: buildplate_node = nozzle_node.getChildNode(buildplate_name) # Fallback mechanism of finding materials: # 1. buildplate-specific material # 2. nozzle-specific material # 3. machine-specific material # 4. generic material (for fdmprinter) nodes_to_check = [buildplate_node, nozzle_node, machine_node, machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)] material_node = None for node in nodes_to_check: if node is not None: material_node = node.material_map.get(root_material_id) if material_node: break return material_node # # Gets MaterialNode for the given extruder and machine with the given material type. # Returns None if: # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str, buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]: node = None machine_definition = global_stack.definition extruder_definition = global_stack.extruders[position].definition if parseBool(machine_definition.getMetaDataEntry("has_materials", False)): material_diameter = extruder_definition.getProperty("material_diameter", "value") if isinstance(material_diameter, SettingFunction): material_diameter = material_diameter(global_stack) # Look at the guid to material dictionary root_material_id = None for material_group in self._guid_material_groups_map[material_guid]: root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", "")) break if not root_material_id: Logger.log("i", "Cannot find materials with guid [%s] ", material_guid) return None node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name, material_diameter, root_material_id) return node # There are 2 ways to get fallback materials; # - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material # - A fallback by GUID; If a material has been duplicated, it should also check if the original materials do have # a GUID. This should only be done if the material itself does not have a quality just yet. def getFallBackMaterialIdsByMaterial(self, material: "InstanceContainer") -> List[str]: results = [] # type: List[str] material_groups = self.getMaterialGroupListByGUID(material.getMetaDataEntry("GUID")) for material_group in material_groups: # type: ignore if material_group.name != material.getId(): # If the material in the group is read only, put it at the front of the list (since that is the most # likely one to get a result) if material_group.is_read_only: results.insert(0, material_group.name) else: results.append(material_group.name) fallback = self.getFallbackMaterialIdByMaterialType(material.getMetaDataEntry("material")) if fallback is not None: results.append(fallback) return results # # Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla". # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use # the generic material IDs to search for qualities. # # An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its # extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine. # A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will # be "generic_pla". This function is intended to get a generic fallback material for the given material type. # # This function returns the generic root material ID for the given material type, where material types are "PLA", # "ABS", etc. # def getFallbackMaterialIdByMaterialType(self, material_type: str) -> Optional[str]: # For safety if material_type not in self._fallback_materials_map: Logger.log("w", "The material type [%s] does not have a fallback material" % material_type) return None fallback_material = self._fallback_materials_map[material_type] if fallback_material: return self.getRootMaterialIDWithoutDiameter(fallback_material["id"]) else: return None ## Get default material for given global stack, extruder position and extruder nozzle name # you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder) def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str], extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]: node = None buildplate_name = global_stack.getBuildplateName() machine_definition = global_stack.definition # The extruder-compatible material diameter in the extruder definition may not be the correct value because # the user can change it in the definition_changes container. if extruder_definition is None: extruder_stack_or_definition = global_stack.extruders[position] is_extruder_stack = True else: extruder_stack_or_definition = extruder_definition is_extruder_stack = False if extruder_stack_or_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)): if is_extruder_stack: material_diameter = extruder_stack_or_definition.getCompatibleMaterialDiameter() else: material_diameter = extruder_stack_or_definition.getProperty("material_diameter", "value") if isinstance(material_diameter, SettingFunction): material_diameter = material_diameter(global_stack) approximate_material_diameter = str(round(material_diameter)) root_material_id = machine_definition.getMetaDataEntry("preferred_material") root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter) node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name, material_diameter, root_material_id) return node def removeMaterialByRootId(self, root_material_id: str): material_group = self.getMaterialGroup(root_material_id) if not material_group: Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id) return nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list for node in nodes_to_remove: self._container_registry.removeContainer(node.getMetaDataEntry("id", "")) # # Methods for GUI # # # Sets the new name for the given material. # @pyqtSlot("QVariant", str) def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: root_material_id = material_node.getMetaDataEntry("base_file") if root_material_id is None: return if self._container_registry.isReadOnly(root_material_id): Logger.log("w", "Cannot set name of read-only container %s.", root_material_id) return material_group = self.getMaterialGroup(root_material_id) if material_group: container = material_group.root_material_node.getContainer() if container: container.setName(name) # # Removes the given material. # @pyqtSlot("QVariant") def removeMaterial(self, material_node: "MaterialNode") -> None: root_material_id = material_node.getMetaDataEntry("base_file") if root_material_id is not None: self.removeMaterialByRootId(root_material_id) # # Creates a duplicate of a material, which has the same GUID and base_file metadata. # Returns the root material ID of the duplicated material if successful. # @pyqtSlot("QVariant", result = str) def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]: root_material_id = cast(str, material_node.getMetaDataEntry("base_file", "")) material_group = self.getMaterialGroup(root_material_id) if not material_group: Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id) return None base_container = material_group.root_material_node.getContainer() if not base_container: return None # Ensure all settings are saved. self._application.saveSettings() # Create a new ID & container to hold the data. new_containers = [] if new_base_id is None: new_base_id = self._container_registry.uniqueName(base_container.getId()) new_base_container = copy.deepcopy(base_container) new_base_container.getMetaData()["id"] = new_base_id new_base_container.getMetaData()["base_file"] = new_base_id if new_metadata is not None: for key, value in new_metadata.items(): new_base_container.getMetaData()[key] = value new_containers.append(new_base_container) # Clone all of them. for node in material_group.derived_material_node_list: container_to_copy = node.getContainer() if not container_to_copy: continue # Create unique IDs for every clone. new_id = new_base_id if container_to_copy.getMetaDataEntry("definition") != "fdmprinter": new_id += "_" + container_to_copy.getMetaDataEntry("definition") if container_to_copy.getMetaDataEntry("variant_name"): nozzle_name = container_to_copy.getMetaDataEntry("variant_name") new_id += "_" + nozzle_name.replace(" ", "_") new_container = copy.deepcopy(container_to_copy) new_container.getMetaData()["id"] = new_id new_container.getMetaData()["base_file"] = new_base_id if new_metadata is not None: for key, value in new_metadata.items(): new_container.getMetaData()[key] = value new_containers.append(new_container) for container_to_add in new_containers: container_to_add.setDirty(True) self._container_registry.addContainer(container_to_add) # if the duplicated material was favorite then the new material should also be added to favorite. if root_material_id in self.getFavorites(): self.addFavorite(new_base_id) return new_base_id # # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID. # Returns the ID of the newly created material. @pyqtSlot(result = str) def createMaterial(self) -> str: from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") # Ensure all settings are saved. self._application.saveSettings() machine_manager = self._application.getMachineManager() extruder_stack = machine_manager.activeStack machine_definition = self._application.getGlobalContainerStack().definition preferred_material = machine_definition.getMetaDataEntry("preferred_material") approximate_diameter = str(extruder_stack.approximateMaterialDiameter) root_material_id = preferred_material if preferred_material else "generic_pla" root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter) material_group = self.getMaterialGroup(root_material_id) if not material_group: # This should never happen Logger.log("w", "Cannot get the material group of %s.", root_material_id) return "" # Create a new ID & container to hold the data. new_id = self._container_registry.uniqueName("custom_material") new_metadata = {"name": catalog.i18nc("@label", "Custom Material"), "brand": catalog.i18nc("@label", "Custom"), "GUID": str(uuid.uuid4()), } self.duplicateMaterial(material_group.root_material_node, new_base_id = new_id, new_metadata = new_metadata) return new_id @pyqtSlot(str) def addFavorite(self, root_material_id: str) -> None: self._favorites.add(root_material_id) self.materialsUpdated.emit() # Ensure all settings are saved. self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites))) self._application.saveSettings() @pyqtSlot(str) def removeFavorite(self, root_material_id: str) -> None: self._favorites.remove(root_material_id) self.materialsUpdated.emit() # Ensure all settings are saved. self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites))) self._application.saveSettings() @pyqtSlot() def getFavorites(self): return self._favorites
class LiveCSS(QWidget): goto_declaration = pyqtSignal(object) def __init__(self, preview, parent=None): QWidget.__init__(self, parent) self.preview = preview self.preview_is_refreshing = False self.refresh_needed = False preview.refresh_starting.connect(self.preview_refresh_starting) preview.refreshed.connect(self.preview_refreshed) self.apply_theme() self.setAutoFillBackground(True) self.update_timer = QTimer(self) self.update_timer.timeout.connect(self.update_data) self.update_timer.setSingleShot(True) self.update_timer.setInterval(500) self.now_showing = (None, None, None) self.stack = s = QStackedLayout(self) self.setLayout(s) self.clear_label = la = QLabel('<h3>' + _( 'No style information found') + '</h3><p>' + _( 'Move the cursor inside a HTML tag to see what styles' ' apply to that tag.')) la.setWordWrap(True) la.setAlignment(Qt.AlignTop | Qt.AlignLeft) s.addWidget(la) self.box = box = Box(self) box.hyperlink_activated.connect(self.goto_declaration, type=Qt.QueuedConnection) self.scroll = sc = QScrollArea(self) sc.setWidget(box) sc.setWidgetResizable(True) s.addWidget(sc) def preview_refresh_starting(self): self.preview_is_refreshing = True def preview_refreshed(self): self.preview_is_refreshing = False # We must let the event loop run otherwise the webview will return # stale data in read_data() self.refresh_needed = True self.start_update_timer() def apply_theme(self): f = self.font() f.setFamily(tprefs['editor_font_family'] or default_font_family()) f.setPointSize(tprefs['editor_font_size']) self.setFont(f) theme = get_theme(tprefs['editor_theme']) pal = self.palette() pal.setColor(pal.Window, theme_color(theme, 'Normal', 'bg')) pal.setColor(pal.WindowText, theme_color(theme, 'Normal', 'fg')) pal.setColor(pal.AlternateBase, theme_color(theme, 'HighlightRegion', 'bg')) pal.setColor(pal.Link, theme_color(theme, 'Link', 'fg')) pal.setColor(pal.LinkVisited, theme_color(theme, 'Keyword', 'fg')) self.setPalette(pal) if hasattr(self, 'box'): self.box.relayout() self.update() def clear(self): self.stack.setCurrentIndex(0) def show_data(self, editor_name, sourceline, tags): if self.preview_is_refreshing: return if sourceline is None: self.clear() else: data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: if editor_name == self.current_name and (editor_name, sourceline, tags) == self.now_showing: # Try again in a little while in case there was a transient # error in the web view self.start_update_timer() return if self.now_showing == (None, None, None) or self.now_showing[0] != self.current_name: self.clear() return # Try to refresh the data for the currently shown tag instead # of clearing editor_name, sourceline, tags = self.now_showing data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: self.clear() return self.now_showing = (editor_name, sourceline, tags) data['html_name'] = editor_name self.box.show_data(data) self.refresh_needed = False self.stack.setCurrentIndex(1) def read_data(self, sourceline, tags): mf = self.preview.view.page().mainFrame() tags = [x.lower() for x in tags] result = unicode_type(mf.evaluateJavaScript( 'window.calibre_preview_integration.live_css(%s, %s)' % ( json.dumps(sourceline), json.dumps(tags))) or '') try: result = json.loads(result) except ValueError: result = None if result is not None: maximum_specificities = {} for node in result['nodes']: is_ancestor = node['is_ancestor'] for rule in node['css']: self.process_rule(rule, is_ancestor, maximum_specificities) for node in result['nodes']: for rule in node['css']: for prop in rule['properties']: if prop.specificity < maximum_specificities[prop.name]: prop.is_overriden = True return result def process_rule(self, rule, is_ancestor, maximum_specificities): selector = rule['selector'] sheet_index = rule['sheet_index'] rule_address = rule['rule_address'] or () if selector is not None: try: specificity = [0] + list(parse(selector)[0].specificity()) except (AttributeError, TypeError, SelectorError): specificity = [0, 0, 0, 0] else: # style attribute specificity = [1, 0, 0, 0] specificity.extend((sheet_index, tuple(rule_address))) ancestor_specificity = 0 if is_ancestor else 1 properties = [] for prop in rule['properties']: important = 1 if prop[-1] == 'important' else 0 p = Property(prop, [ancestor_specificity] + [important] + specificity) properties.append(p) if p.specificity > maximum_specificities.get(p.name, (0,0,0,0,0,0)): maximum_specificities[p.name] = p.specificity rule['properties'] = properties href = rule['href'] if hasattr(href, 'startswith') and href.startswith('%s://%s' % (FAKE_PROTOCOL, FAKE_HOST)): qurl = QUrl(href) name = qurl.path()[1:] if name: rule['href'] = name @property def current_name(self): return self.preview.current_name @property def is_visible(self): return self.isVisible() def showEvent(self, ev): self.update_timer.start() actions['auto-reload-preview'].setEnabled(True) return QWidget.showEvent(self, ev) def sync_to_editor(self): self.update_data() def update_data(self): if not self.is_visible or self.preview_is_refreshing: return editor_name = self.current_name ed = editors.get(editor_name, None) if self.update_timer.isActive() or (ed is None and editor_name is not None): return QTimer.singleShot(100, self.update_data) if ed is not None: sourceline, tags = ed.current_tag(for_position_sync=False) if self.refresh_needed or self.now_showing != (editor_name, sourceline, tags): self.show_data(editor_name, sourceline, tags) def start_update_timer(self): if self.is_visible: self.update_timer.start() def stop_update_timer(self): self.update_timer.stop() def navigate_to_declaration(self, data, editor): if data['type'] == 'inline': sourceline, tags = data['sourceline_address'] editor.goto_sourceline(sourceline, tags, attribute='style') elif data['type'] == 'sheet': editor.goto_css_rule(data['rule_address']) elif data['type'] == 'elem': editor.goto_css_rule(data['rule_address'], sourceline_address=data['sourceline_address'])
class SearchDialog(QDialog, Ui_Dialog): SEARCH_TEXT = _('&Search') STOP_TEXT = _('&Stop') def __init__(self, gui, parent=None, query=''): QDialog.__init__(self, parent) self.setupUi(self) self.config = JSONConfig('store/search') self.search_title.initialize('store_search_search_title') self.search_author.initialize('store_search_search_author') self.search_edit.initialize('store_search_search') # Loads variables that store various settings. # This needs to be called soon in __init__ because # the variables it sets up are used later. self.load_settings() self.gui = gui # Setup our worker threads. self.search_pool = SearchThreadPool(self.search_thread_count) self.cache_pool = CacheUpdateThreadPool(self.cache_thread_count) self.results_view.model().cover_pool.set_thread_count( self.cover_thread_count) self.results_view.model().details_pool.set_thread_count( self.details_thread_count) self.results_view.setCursor(Qt.PointingHandCursor) # Check for results and hung threads. self.checker = QTimer() self.progress_checker = QTimer() self.hang_check = 0 # Update store caches silently. for p in list(self.gui.istores.values()): self.cache_pool.add_task(p, self.timeout) self.store_checks = {} self.setup_store_checks() # Set the search query if isinstance(query, str): self.search_edit.setText(query) elif isinstance(query, dict): if 'author' in query: self.search_author.setText(query['author']) if 'title' in query: self.search_title.setText(query['title']) # Create and add the progress indicator self.pi = ProgressIndicator(self, 24) self.button_layout.takeAt(0) self.button_layout.setAlignment(Qt.AlignCenter) self.button_layout.insertWidget(0, self.pi, 0, Qt.AlignCenter) self.adv_search_button.setIcon(QIcon(I('gear.png'))) self.adv_search_button.setToolTip(_('Advanced search')) self.configure.setIcon(QIcon(I('config.png'))) self.adv_search_button.clicked.connect(self.build_adv_search) self.search.clicked.connect(self.toggle_search) self.checker.timeout.connect(self.get_results) self.progress_checker.timeout.connect(self.check_progress) self.results_view.activated.connect(self.result_item_activated) self.results_view.download_requested.connect(self.download_book) self.results_view.open_requested.connect(self.open_store) self.results_view.model().total_changed.connect(self.update_book_total) self.select_all_stores.clicked.connect(self.stores_select_all) self.select_invert_stores.clicked.connect(self.stores_select_invert) self.select_none_stores.clicked.connect(self.stores_select_none) self.configure.clicked.connect(self.do_config) self.finished.connect(self.dialog_closed) self.searching = False self.progress_checker.start(100) self.restore_state() def setup_store_checks(self): first_run = self.config.get('first_run', True) # Add check boxes for each store so the user # can disable searching specific stores on a # per search basis. existing = {} for n in self.store_checks: existing[n] = self.store_checks[n].isChecked() self.store_checks = {} stores_check_widget = QWidget() store_list_layout = QGridLayout() stores_check_widget.setLayout(store_list_layout) icon = QIcon(I('donate.png')) for i, x in enumerate( sorted(list(self.gui.istores.keys()), key=lambda x: x.lower())): cbox = QCheckBox(x) cbox.setChecked(existing.get(x, first_run)) store_list_layout.addWidget(cbox, i, 0, 1, 1) if self.gui.istores[x].base_plugin.affiliate: iw = QLabel(self) iw.setToolTip('<p>' + _( 'Buying from this store supports the calibre developer: %s</p>' ) % self.gui.istores[x].base_plugin.author + '</p>') iw.setPixmap(icon.pixmap(16, 16)) store_list_layout.addWidget(iw, i, 1, 1, 1) self.store_checks[x] = cbox store_list_layout.setRowStretch(store_list_layout.rowCount(), 10) self.store_list.setWidget(stores_check_widget) self.config['first_run'] = False def build_adv_search(self): adv = AdvSearchBuilderDialog(self) if adv.exec_() == QDialog.Accepted: self.search_edit.setText(adv.search_string()) def resize_columns(self): total = 600 # Cover self.results_view.setColumnWidth(0, 85) total = total - 85 # Title / Author self.results_view.setColumnWidth(1, int(total * .40)) # Price self.results_view.setColumnWidth(2, int(total * .12)) # DRM self.results_view.setColumnWidth(3, int(total * .15)) # Store / Formats self.results_view.setColumnWidth(4, int(total * .25)) # Download self.results_view.setColumnWidth(5, 20) # Affiliate self.results_view.setColumnWidth(6, 20) def toggle_search(self): if self.searching: self.search_pool.abort() m = self.results_view.model() m.details_pool.abort() m.cover_pool.abort() self.search.setText(self.SEARCH_TEXT) self.checker.stop() self.searching = False else: self.do_search() # Prevent hitting the enter key twice in quick succession causing # the search to start and stop self.search.setEnabled(False) QTimer.singleShot(1000, lambda: self.search.setEnabled(True)) def do_search(self): # Stop all running threads. self.checker.stop() self.search_pool.abort() # Clear the visible results. self.results_view.model().clear_results() # Don't start a search if there is nothing to search for. query = [] if self.search_title.text(): query.append('title2:"~%s"' % str(self.search_title.text()).replace('"', ' ')) if self.search_author.text(): query.append('author2:"%s"' % str(self.search_author.text()).replace('"', ' ')) if self.search_edit.text(): query.append(str(self.search_edit.text())) query = " ".join(query) if not query.strip(): error_dialog(self, _('No query'), _('You must enter a title, author or keyword to' ' search for.'), show=True) return self.searching = True self.search.setText(self.STOP_TEXT) # Give the query to the results model so it can do # futher filtering. self.results_view.model().set_query(query) # Plugins are in random order that does not change. # Randomize the ord of the plugin names every time # there is a search. This way plugins closer # to a don't have an unfair advantage over # plugins further from a. store_names = list(self.store_checks.keys()) if not store_names: return # Remove all of our internal filtering logic from the query. query = self.clean_query(query) shuffle(store_names) # Add plugins that the user has checked to the search pool's work queue. self.gui.istores.join(4.0) # Wait for updated plugins to load for n in store_names: if self.store_checks[n].isChecked(): self.search_pool.add_task(query, n, self.gui.istores[n], self.max_results, self.timeout) self.hang_check = 0 self.checker.start(100) self.pi.startAnimation() def clean_query(self, query): query = query.lower() # Remove control modifiers. query = query.replace('\\', '') query = query.replace('!', '') query = query.replace('=', '') query = query.replace('~', '') query = query.replace('>', '') query = query.replace('<', '') # Remove the prefix. for loc in ('all', 'author', 'author2', 'authors', 'title', 'title2'): query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query) query = query.replace('%s:' % loc, '') # Remove the prefix and search text. for loc in ('cover', 'download', 'downloads', 'drm', 'format', 'formats', 'price', 'store'): query = re.sub(r'%s:"[^"]"' % loc, '', query) query = re.sub(r'%s:[^\s]*' % loc, '', query) # Remove logic. query = re.sub(r'(^|\s|")(and|not|or|a|the|is|of)(\s|$|")', r' ', query) # Remove " query = query.replace('"', '') # Remove excess whitespace. query = re.sub(r'\s+', ' ', query) query = query.strip() return query.encode('utf-8') def save_state(self): self.config['geometry'] = bytearray(self.saveGeometry()) self.config['store_splitter_state'] = bytearray( self.store_splitter.saveState()) self.config['results_view_column_width'] = [ self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount()) ] self.config['sort_col'] = self.results_view.model().sort_col self.config['sort_order'] = self.results_view.model().sort_order self.config['open_external'] = self.open_external.isChecked() store_check = {} for k, v in list(self.store_checks.items()): store_check[k] = v.isChecked() self.config['store_checked'] = store_check def restore_state(self): geometry = self.config.get('geometry', None) if geometry: self.restoreGeometry(geometry) splitter_state = self.config.get('store_splitter_state', None) if splitter_state: self.store_splitter.restoreState(splitter_state) results_cwidth = self.config.get('results_view_column_width', None) if results_cwidth: for i, x in enumerate(results_cwidth): if i >= self.results_view.model().columnCount(): break self.results_view.setColumnWidth(i, x) else: self.resize_columns() self.open_external.setChecked(self.should_open_external) store_check = self.config.get('store_checked', None) if store_check: for n in store_check: if n in self.store_checks: self.store_checks[n].setChecked(store_check[n]) self.results_view.model().sort_col = self.config.get('sort_col', 2) self.results_view.model().sort_order = self.config.get( 'sort_order', Qt.AscendingOrder) self.results_view.header().setSortIndicator( self.results_view.model().sort_col, self.results_view.model().sort_order) def load_settings(self): # Seconds self.timeout = self.config.get('timeout', 75) # Milliseconds self.hang_time = self.config.get('hang_time', 75) * 1000 self.max_results = self.config.get('max_results', 15) self.should_open_external = self.config.get('open_external', True) # Number of threads to run for each type of operation self.search_thread_count = self.config.get('search_thread_count', 4) self.cache_thread_count = self.config.get('cache_thread_count', 2) self.cover_thread_count = self.config.get('cover_thread_count', 2) self.details_thread_count = self.config.get('details_thread_count', 4) def do_config(self): # Save values that need to be synced between the dialog and the # search widget. self.config['open_external'] = self.open_external.isChecked() # Create the config dialog. It's going to put two config widgets # into a QTabWidget for displaying all of the settings. d = QDialog(self) button_box = QDialogButtonBox(QDialogButtonBox.Close) v = QVBoxLayout(d) button_box.accepted.connect(d.accept) button_box.rejected.connect(d.reject) d.setWindowTitle(_('Customize Get books search')) tab_widget = QTabWidget(d) v.addWidget(tab_widget) v.addWidget(button_box) chooser_config_widget = StoreChooserWidget() search_config_widget = StoreConfigWidget(self.config) tab_widget.addTab(chooser_config_widget, _('Choose stores')) tab_widget.addTab(search_config_widget, _('Configure search')) # Restore dialog state. geometry = self.config.get('config_dialog_geometry', None) if geometry: d.restoreGeometry(geometry) else: d.resize(800, 600) tab_index = self.config.get('config_dialog_tab_index', 0) tab_index = min(tab_index, tab_widget.count() - 1) tab_widget.setCurrentIndex(tab_index) d.exec_() # Save dialog state. self.config['config_dialog_geometry'] = bytearray(d.saveGeometry()) self.config['config_dialog_tab_index'] = tab_widget.currentIndex() search_config_widget.save_settings() self.config_changed() self.gui.load_store_plugins() self.setup_store_checks() def config_changed(self): self.load_settings() self.open_external.setChecked(self.should_open_external) self.search_pool.set_thread_count(self.search_thread_count) self.cache_pool.set_thread_count(self.cache_thread_count) self.results_view.model().cover_pool.set_thread_count( self.cover_thread_count) self.results_view.model().details_pool.set_thread_count( self.details_thread_count) def get_results(self): # We only want the search plugins to run # a maximum set amount of time before giving up. self.hang_check += 1 if self.hang_check >= self.hang_time: self.search_pool.abort() self.checker.stop() else: # Stop the checker if not threads are running. if not self.search_pool.threads_running( ) and not self.search_pool.has_tasks(): self.checker.stop() while self.search_pool.has_results(): res, store_plugin = self.search_pool.get_result() if res: self.results_view.model().add_result(res, store_plugin) if not self.search_pool.threads_running( ) and not self.results_view.model().has_results(): info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False) def update_book_total(self, total): self.total.setText('%s' % total) def result_item_activated(self, index): result = self.results_view.model().get_result(index) if result.downloads: self.download_book(result) else: self.open_store(result) def download_book(self, result): d = ChooseFormatDialog(self, _('Choose format to download to your library.'), list(result.downloads.keys())) if d.exec_() == d.Accepted: ext = d.format() fname = result.title[:60] + '.' + ext.lower() fname = ascii_filename(fname) show_download_info(result.title, parent=self) self.gui.download_ebook(result.downloads[ext], filename=fname, create_browser=result.create_browser) def open_store(self, result): self.gui.istores[result.store_name].open( self, result.detail_item, self.open_external.isChecked()) def check_progress(self): m = self.results_view.model() if not self.search_pool.threads_running( ) and not m.cover_pool.threads_running( ) and not m.details_pool.threads_running(): self.pi.stopAnimation() self.search.setText(self.SEARCH_TEXT) self.searching = False else: self.searching = True if str(self.search.text()) != self.STOP_TEXT: self.search.setText(self.STOP_TEXT) if not self.pi.isAnimated(): self.pi.startAnimation() def stores_select_all(self): for check in list(self.store_checks.values()): check.setChecked(True) def stores_select_invert(self): for check in list(self.store_checks.values()): check.setChecked(not check.isChecked()) def stores_select_none(self): for check in list(self.store_checks.values()): check.setChecked(False) def dialog_closed(self, result): self.results_view.model().closing() self.search_pool.abort() self.cache_pool.abort() self.save_state() def exec_(self): if str(self.search_edit.text()).strip() or str( self.search_title.text()).strip() or str( self.search_author.text()).strip(): self.do_search() return QDialog.exec_(self)
class TagBrowserWidget(QFrame): # {{{ def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle(QFrame.Shape.NoFrame if gprefs['tag_browser_old_look'] else QFrame.Shape.StyledPanel) self._parent = parent self._layout = QVBoxLayout(self) self._layout.setContentsMargins(0,0,0,0) # Set up the find box & button self.tb_bar = tbb = TagBrowserBar(self) tbb.clear_find.connect(self.reset_find) self.alter_tb, self.item_search, self.search_button = tbb.alter_tb, tbb.item_search, tbb.search_button self.toggle_search_button = tbb.toggle_search_button self._layout.addWidget(tbb) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[str].connect(self.do_find) # The tags view parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self._layout.insertWidget(0, parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.Shape.StyledPanel) l.setAutoFillBackground(True) l.setText('<p><b>'+_('No more matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignmentFlag.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10,20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect(self.not_found_label_timer_event, type=Qt.ConnectionType.QueuedConnection) # The Alter Tag Browser button l = self.alter_tb self.collapse_all_action = ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser collapse all', _('Collapse all'), default_keys=(), action=ac, group=_('Tag browser')) connect_lambda(ac.triggered, self, lambda self: self.tags_view.collapseAll()) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser alter', _('Configure Tag browser'), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(l.showMenu) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate((_('Name'), _('Number of books'), _('Average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip( _('Set the sort order for entries in the Tag browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate((_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc.')) mt.setToolTip(_('All of these category_managers are available by right-clicking ' 'on items in the Tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser toggle item', _("'Click' found item"), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.toggle_item) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser set focus', _("Give the Tag browser keyboard focus"), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.give_tb_focus) # self.leak_test_timer = QTimer(self) # self.leak_test_timer.timeout.connect(self.test_for_leak) # self.leak_test_timer.start(5000) def save_state(self): gprefs.set('tag browser search box visible', self.toggle_search_button.isChecked()) def toggle_item(self): self.tags_view.toggle_current_index() def give_tb_focus(self, *args): if gprefs['tag_browser_allow_keyboard_focus']: tb = self.tags_view if tb.hasFocus(): self._parent.shift_esc() elif self._parent.current_view() == self._parent.library_view: tb.setFocus() idx = tb.currentIndex() if not idx.isValid(): idx = tb.model().createIndex(0, 0) tb.setCurrentIndex(idx) def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) if not to_what: self._parent.shift_esc() def find_text_changed(self, str_): self.current_find_position = None def set_focus_to_find_box(self): self.tb_bar.set_focus_to_find_box() def do_find(self, str_=None): self.current_find_position = None self.find() @property def find_text(self): return unicode_type(self.item_search.currentText()).strip() def reset_find(self): model = self.tags_view.model() model.clear_boxed() if model.get_categories_filter(): model.set_categories_filter(None) self.tags_view.recount() self.current_find_position = None def find(self): model = self.tags_view.model() model.clear_boxed() # When a key is specified don't use the auto-collapsing search. # A colon separates the lookup key from the search string. # A leading colon says not to use autocollapsing search but search all keys txt = self.find_text colon = txt.find(':') if colon >= 0: key = self._parent.library_view.model().db.\ field_metadata.search_term_to_field_key(txt[:colon]) if key in self._parent.library_view.model().db.field_metadata: txt = txt[colon+1:] else: key = '' txt = txt[1:] if colon == 0 else txt else: key = None # key is None indicates that no colon was found. # key == '' means either a leading : was found or the key is invalid # At this point the txt might have a leading =, in which case do an # exact match search if (gprefs.get('tag_browser_always_autocollapse', False) and key is None and not txt.startswith('*')): txt = '*' + txt if txt.startswith('*'): self.tags_view.collapseAll() model.set_categories_filter(txt[1:]) self.tags_view.recount() self.current_find_position = None return if model.get_categories_filter(): model.set_categories_filter(None) self.tags_view.recount() self.current_find_position = None if not txt: return self.item_search.lineEdit().blockSignals(True) self.search_button.setFocus(True) self.item_search.lineEdit().blockSignals(False) if txt.startswith('='): equals_match = True txt = txt[1:] else: equals_match = False self.current_find_position = \ model.find_item_node(key, txt, self.current_find_position, equals_match=equals_match) if self.current_find_position: self.tags_view.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) if self.tags_view.verticalScrollBar().isVisible(): sbw = self.tags_view.verticalScrollBar().width() else: sbw = 0 width = self.width() - 8 - sbw height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) self.not_found_label.move(4, 10) self.not_found_label_timer.start(2000) def not_found_label_timer_event(self): self.not_found_label.setVisible(False) def keyPressEvent(self, ev): if ev.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) and self.find_text: self.find() ev.accept() return return QFrame.keyPressEvent(self, ev)
class SearchDialog(QDialog, Ui_Dialog): SEARCH_TEXT = _('&Search') STOP_TEXT = _('&Stop') def __init__(self, gui, parent=None, query=''): QDialog.__init__(self, parent) self.setupUi(self) self.config = JSONConfig('store/search') self.search_title.initialize('store_search_search_title') self.search_author.initialize('store_search_search_author') self.search_edit.initialize('store_search_search') # Loads variables that store various settings. # This needs to be called soon in __init__ because # the variables it sets up are used later. self.load_settings() self.gui = gui # Setup our worker threads. self.search_pool = SearchThreadPool(self.search_thread_count) self.cache_pool = CacheUpdateThreadPool(self.cache_thread_count) self.results_view.model().cover_pool.set_thread_count(self.cover_thread_count) self.results_view.model().details_pool.set_thread_count(self.details_thread_count) self.results_view.setCursor(Qt.PointingHandCursor) # Check for results and hung threads. self.checker = QTimer() self.progress_checker = QTimer() self.hang_check = 0 # Update store caches silently. for p in self.gui.istores.values(): self.cache_pool.add_task(p, self.timeout) self.store_checks = {} self.setup_store_checks() # Set the search query if isinstance(query, (bytes, unicode_type)): self.search_edit.setText(query) elif isinstance(query, dict): if 'author' in query: self.search_author.setText(query['author']) if 'title' in query: self.search_title.setText(query['title']) # Create and add the progress indicator self.pi = ProgressIndicator(self, 24) self.button_layout.takeAt(0) self.button_layout.setAlignment(Qt.AlignCenter) self.button_layout.insertWidget(0, self.pi, 0, Qt.AlignCenter) self.adv_search_button.setIcon(QIcon(I('gear.png'))) self.adv_search_button.setToolTip(_('Advanced search')) self.configure.setIcon(QIcon(I('config.png'))) self.adv_search_button.clicked.connect(self.build_adv_search) self.search.clicked.connect(self.toggle_search) self.checker.timeout.connect(self.get_results) self.progress_checker.timeout.connect(self.check_progress) self.results_view.activated.connect(self.result_item_activated) self.results_view.download_requested.connect(self.download_book) self.results_view.open_requested.connect(self.open_store) self.results_view.model().total_changed.connect(self.update_book_total) self.select_all_stores.clicked.connect(self.stores_select_all) self.select_invert_stores.clicked.connect(self.stores_select_invert) self.select_none_stores.clicked.connect(self.stores_select_none) self.configure.clicked.connect(self.do_config) self.finished.connect(self.dialog_closed) self.searching = False self.progress_checker.start(100) self.restore_state() def setup_store_checks(self): first_run = self.config.get('first_run', True) # Add check boxes for each store so the user # can disable searching specific stores on a # per search basis. existing = {} for n in self.store_checks: existing[n] = self.store_checks[n].isChecked() self.store_checks = {} stores_check_widget = QWidget() store_list_layout = QGridLayout() stores_check_widget.setLayout(store_list_layout) icon = QIcon(I('donate.png')) for i, x in enumerate(sorted(self.gui.istores.keys(), key=lambda x: x.lower())): cbox = QCheckBox(x) cbox.setChecked(existing.get(x, first_run)) store_list_layout.addWidget(cbox, i, 0, 1, 1) if self.gui.istores[x].base_plugin.affiliate: iw = QLabel(self) iw.setToolTip('<p>' + _('Buying from this store supports the calibre developer: %s</p>') % self.gui.istores[x].base_plugin.author + '</p>') iw.setPixmap(icon.pixmap(16, 16)) store_list_layout.addWidget(iw, i, 1, 1, 1) self.store_checks[x] = cbox store_list_layout.setRowStretch(store_list_layout.rowCount(), 10) self.store_list.setWidget(stores_check_widget) self.config['first_run'] = False def build_adv_search(self): adv = AdvSearchBuilderDialog(self) if adv.exec_() == QDialog.Accepted: self.search_edit.setText(adv.search_string()) def resize_columns(self): total = 600 # Cover self.results_view.setColumnWidth(0, 85) total = total - 85 # Title / Author self.results_view.setColumnWidth(1,int(total*.40)) # Price self.results_view.setColumnWidth(2,int(total*.12)) # DRM self.results_view.setColumnWidth(3, int(total*.15)) # Store / Formats self.results_view.setColumnWidth(4, int(total*.25)) # Download self.results_view.setColumnWidth(5, 20) # Affiliate self.results_view.setColumnWidth(6, 20) def toggle_search(self): if self.searching: self.search_pool.abort() m = self.results_view.model() m.details_pool.abort() m.cover_pool.abort() self.search.setText(self.SEARCH_TEXT) self.checker.stop() self.searching = False else: self.do_search() # Prevent hitting the enter key twice in quick succession causing # the search to start and stop self.search.setEnabled(False) QTimer.singleShot(1000, lambda :self.search.setEnabled(True)) def do_search(self): # Stop all running threads. self.checker.stop() self.search_pool.abort() # Clear the visible results. self.results_view.model().clear_results() # Don't start a search if there is nothing to search for. query = [] if self.search_title.text(): query.append(u'title2:"~%s"' % unicode_type(self.search_title.text()).replace('"', ' ')) if self.search_author.text(): query.append(u'author2:"%s"' % unicode_type(self.search_author.text()).replace('"', ' ')) if self.search_edit.text(): query.append(unicode_type(self.search_edit.text())) query = " ".join(query) if not query.strip(): error_dialog(self, _('No query'), _('You must enter a title, author or keyword to' ' search for.'), show=True) return self.searching = True self.search.setText(self.STOP_TEXT) # Give the query to the results model so it can do # futher filtering. self.results_view.model().set_query(query) # Plugins are in random order that does not change. # Randomize the ord of the plugin names every time # there is a search. This way plugins closer # to a don't have an unfair advantage over # plugins further from a. store_names = list(self.store_checks) if not store_names: return # Remove all of our internal filtering logic from the query. query = self.clean_query(query) shuffle(store_names) # Add plugins that the user has checked to the search pool's work queue. self.gui.istores.join(4.0) # Wait for updated plugins to load for n in store_names: if self.store_checks[n].isChecked(): self.search_pool.add_task(query, n, self.gui.istores[n], self.max_results, self.timeout) self.hang_check = 0 self.checker.start(100) self.pi.startAnimation() def clean_query(self, query): query = query.lower() # Remove control modifiers. query = query.replace('\\', '') query = query.replace('!', '') query = query.replace('=', '') query = query.replace('~', '') query = query.replace('>', '') query = query.replace('<', '') # Remove the prefix. for loc in ('all', 'author', 'author2', 'authors', 'title', 'title2'): query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, r'\g<a>', query) query = query.replace('%s:' % loc, '') # Remove the prefix and search text. for loc in ('cover', 'download', 'downloads', 'drm', 'format', 'formats', 'price', 'store'): query = re.sub(r'%s:"[^"]"' % loc, '', query) query = re.sub(r'%s:[^\s]*' % loc, '', query) # Remove logic. query = re.sub(r'(^|\s|")(and|not|or|a|the|is|of)(\s|$|")', r' ', query) # Remove " query = query.replace('"', '') # Remove excess whitespace. query = re.sub(r'\s+', ' ', query) query = query.strip() return query.encode('utf-8') def save_state(self): self.config['geometry'] = bytearray(self.saveGeometry()) self.config['store_splitter_state'] = bytearray(self.store_splitter.saveState()) self.config['results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())] self.config['sort_col'] = self.results_view.model().sort_col self.config['sort_order'] = self.results_view.model().sort_order self.config['open_external'] = self.open_external.isChecked() store_check = {} for k, v in self.store_checks.items(): store_check[k] = v.isChecked() self.config['store_checked'] = store_check def restore_state(self): geometry = self.config.get('geometry', None) if geometry: self.restoreGeometry(geometry) splitter_state = self.config.get('store_splitter_state', None) if splitter_state: self.store_splitter.restoreState(splitter_state) results_cwidth = self.config.get('results_view_column_width', None) if results_cwidth: for i, x in enumerate(results_cwidth): if i >= self.results_view.model().columnCount(): break self.results_view.setColumnWidth(i, x) else: self.resize_columns() self.open_external.setChecked(self.should_open_external) store_check = self.config.get('store_checked', None) if store_check: for n in store_check: if n in self.store_checks: self.store_checks[n].setChecked(store_check[n]) self.results_view.model().sort_col = self.config.get('sort_col', 2) self.results_view.model().sort_order = self.config.get('sort_order', Qt.AscendingOrder) self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order) def load_settings(self): # Seconds self.timeout = self.config.get('timeout', 75) # Milliseconds self.hang_time = self.config.get('hang_time', 75) * 1000 self.max_results = self.config.get('max_results', 15) self.should_open_external = self.config.get('open_external', True) # Number of threads to run for each type of operation self.search_thread_count = self.config.get('search_thread_count', 4) self.cache_thread_count = self.config.get('cache_thread_count', 2) self.cover_thread_count = self.config.get('cover_thread_count', 2) self.details_thread_count = self.config.get('details_thread_count', 4) def do_config(self): # Save values that need to be synced between the dialog and the # search widget. self.config['open_external'] = self.open_external.isChecked() # Create the config dialog. It's going to put two config widgets # into a QTabWidget for displaying all of the settings. d = QDialog(self) button_box = QDialogButtonBox(QDialogButtonBox.Close) v = QVBoxLayout(d) button_box.accepted.connect(d.accept) button_box.rejected.connect(d.reject) d.setWindowTitle(_('Customize Get books search')) tab_widget = QTabWidget(d) v.addWidget(tab_widget) v.addWidget(button_box) chooser_config_widget = StoreChooserWidget() search_config_widget = StoreConfigWidget(self.config) tab_widget.addTab(chooser_config_widget, _('Choose s&tores')) tab_widget.addTab(search_config_widget, _('Configure s&earch')) # Restore dialog state. geometry = self.config.get('config_dialog_geometry', None) if geometry: d.restoreGeometry(geometry) else: d.resize(800, 600) tab_index = self.config.get('config_dialog_tab_index', 0) tab_index = min(tab_index, tab_widget.count() - 1) tab_widget.setCurrentIndex(tab_index) d.exec_() # Save dialog state. self.config['config_dialog_geometry'] = bytearray(d.saveGeometry()) self.config['config_dialog_tab_index'] = tab_widget.currentIndex() search_config_widget.save_settings() self.config_changed() self.gui.load_store_plugins() self.setup_store_checks() def config_changed(self): self.load_settings() self.open_external.setChecked(self.should_open_external) self.search_pool.set_thread_count(self.search_thread_count) self.cache_pool.set_thread_count(self.cache_thread_count) self.results_view.model().cover_pool.set_thread_count(self.cover_thread_count) self.results_view.model().details_pool.set_thread_count(self.details_thread_count) def get_results(self): # We only want the search plugins to run # a maximum set amount of time before giving up. self.hang_check += 1 if self.hang_check >= self.hang_time: self.search_pool.abort() self.checker.stop() else: # Stop the checker if not threads are running. if not self.search_pool.threads_running() and not self.search_pool.has_tasks(): self.checker.stop() while self.search_pool.has_results(): res, store_plugin = self.search_pool.get_result() if res: self.results_view.model().add_result(res, store_plugin) if not self.search_pool.threads_running() and not self.results_view.model().has_results(): info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False) def update_book_total(self, total): self.total.setText('%s' % total) def result_item_activated(self, index): result = self.results_view.model().get_result(index) if result.downloads: self.download_book(result) else: self.open_store(result) def download_book(self, result): d = ChooseFormatDialog(self, _('Choose format to download to your library.'), result.downloads.keys()) if d.exec_() == d.Accepted: ext = d.format() fname = result.title[:60] + '.' + ext.lower() fname = ascii_filename(fname) show_download_info(result.title, parent=self) self.gui.download_ebook(result.downloads[ext], filename=fname, create_browser=result.create_browser) def open_store(self, result): self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked()) def check_progress(self): m = self.results_view.model() if not self.search_pool.threads_running() and not m.cover_pool.threads_running() and not m.details_pool.threads_running(): self.pi.stopAnimation() self.search.setText(self.SEARCH_TEXT) self.searching = False else: self.searching = True if unicode_type(self.search.text()) != self.STOP_TEXT: self.search.setText(self.STOP_TEXT) if not self.pi.isAnimated(): self.pi.startAnimation() def stores_select_all(self): for check in self.store_checks.values(): check.setChecked(True) def stores_select_invert(self): for check in self.store_checks.values(): check.setChecked(not check.isChecked()) def stores_select_none(self): for check in self.store_checks.values(): check.setChecked(False) def dialog_closed(self, result): self.results_view.model().closing() self.search_pool.abort() self.cache_pool.abort() self.save_state() def exec_(self): if unicode_type(self.search_edit.text()).strip() or unicode_type(self.search_title.text()).strip() or unicode_type(self.search_author.text()).strip(): self.do_search() return QDialog.exec_(self)
class BeamMonitor(QWidget): AmplitudeSender = pyqtSignal(list) QMSetParameters = pyqtSignal(str, float, float, float) def __init__(self): super(BeamMonitor, self).__init__() self.timer = QTimer(self) self.timer.timeout.connect(self.getDatafromEPICS) self.InitUI() self.QMCurrent = SinCurrent.CurrentThread() self.QMSetParameters.connect(self.QMCurrent.receiveParameters) def InitUI(self): global QMCurrentAmplitude, QMCycle, QMInterval, QMChannelName, DMCurrentAmplitude, DMInterval, DMChannelName self.BPMChannelName_X = "xuliang" self.BPMChannelName_Y = "xuliang" DMChannelName = "xuliang" QMChannelName = "xuliang" QMCurrentAmplitude = 0 QMCycle = 1 QMInterval = 0 DMCurrentAmplitude = 0 DMInterval = 0 self.FilePath = ["", ""] self.error_X = [] self.error_Y = [] self.sinCurrentRun = False self.DMCurrentRun = False self.BPMChannelRight = False self.QMChannelRight = False self.DMChannelRight = False self.ui = mainwindow.Ui_Form() self.ui.setupUi(self) self.ui.SetSaveInfo.hide() self.ui.FilePath.setReadOnly(True) self.ui.isSaveCheckBox.stateChanged.connect( self.on_isSaveCheckBox_slot) self.ui.BPMChannel_X.textChanged.connect(self.getChannelName) self.ui.BPMChannel_Y.textChanged.connect(self.getChannelName) self.ui.QMChannel.textChanged.connect(self.getChannelName) self.ui.DMChannel.textChanged.connect(self.getChannelName) self.ui.StartBtn.clicked.connect(self.on_StartBtn_slot) self.ui.StopBtn.clicked.connect(self.on_StopBtn_slot) self.ui.QMPutBtn.clicked.connect(self.QMCurrent_set) self.ui.DMPutBtn.clicked.connect(self.DMCurrent_set) self.ui.FilePathBtn.clicked.connect(self.getFilePath) self.ui.QM_A.valueChanged.connect(self.getValue) self.ui.QM_C.valueChanged.connect(self.getValue) self.ui.QM_I.valueChanged.connect(self.getValue) self.ui.DM_C.valueChanged.connect(self.getValue) self.ui.DM_I.valueChanged.connect(self.getValue) self.XLine = XOnBPM() self.VLayout = QVBoxLayout() self.VLayout.addWidget(self.XLine) self.ui.XFigure.setLayout(self.VLayout) self.t = 0 def on_StartBtn_slot(self): #每0.5秒读取一次BPM的数据 self.timer.start(500) self.ui.StartBtn.setEnabled(False) self.ui.isSaveCheckBox.setEnabled(False) self.ui.BPMChannel_X.setReadOnly(True) self.ui.BPMChannel_Y.setReadOnly(True) def on_StopBtn_slot(self): self.timer.stop() self.t = 0 self.XLine.TimeAndX = np.empty((0, 3)) self.ui.StartBtn.setEnabled(True) self.ui.isSaveCheckBox.setEnabled(True) self.ui.BPMChannel_X.setReadOnly(False) self.ui.BPMChannel_Y.setReadOnly(False) def on_isSaveCheckBox_slot(self): ''' 是否保存数据,保存则显示设置文件名,文件路径 :return: ''' if self.ui.isSaveCheckBox.isChecked(): self.ui.SetSaveInfo.show() if self.FilePath[0] == "": self.ui.StartBtn.setEnabled(False) else: self.ui.SetSaveInfo.hide() self.ui.StartBtn.setEnabled(True) def getDatafromEPICS(self): ''' 从EPICS系统中读取数据 :return: ''' global QMCycle if not self.BPMChannelRight: if (None == caget(self.BPMChannelName_X, timeout=1)) or (None == caget( self.BPMChannelName_Y, timeout=1)): QMessageBox.information(self, "提示", "无法连接到通道,请检查是否有误!") self.timer.stop() self.t = 0 self.XLine.TimeAndX = np.empty((0, 3)) self.ui.StartBtn.setEnabled(True) self.ui.isSaveCheckBox.setEnabled(True) self.ui.BPMChannel_X.setReadOnly(False) self.ui.BPMChannel_Y.setReadOnly(False) else: self.BPMChannelRight = True if self.BPMChannelRight: current_X = caget(self.BPMChannelName_X) current_Y = caget(self.BPMChannelName_Y) print([self.t * 0.5, current_X, current_Y]) if self.XLine.TimeAndX.shape[0] < 80: self.XLine.TimeAndX = np.vstack( (self.XLine.TimeAndX, [self.t * 0.5, current_X, current_Y])) if self.ui.isSaveCheckBox.isChecked(): with open(self.FilePath[0], 'ab') as f: np.savetxt( f, np.array([[self.t * 0.5, current_X, current_Y]])) else: self.XLine.TimeAndX = np.delete(self.XLine.TimeAndX, 0, axis=0) self.XLine.TimeAndX = np.vstack( (self.XLine.TimeAndX, [self.t * 0.5, current_X, current_Y])) if self.ui.isSaveCheckBox.isChecked(): with open(self.FilePath[0], 'ab') as f: np.savetxt( f, np.array([[self.t * 0.5, current_X, current_Y]])) if (self.t + 1) % (QMCycle * 2) == 0: if len(self.error_X) < 2: self.error_X.append( max(self.XLine.TimeAndX[(self.t + 1 - QMCycle * 2):self.t + 1, 1]) - min(self.XLine.TimeAndX[(self.t + 1 - QMCycle * 2):self.t + 1, 1])) self.error_Y.append( max(self.XLine.TimeAndX[(self.t + 1 - QMCycle * 2):self.t + 1, 2]) - min(self.XLine.TimeAndX[(self.t + 1 - QMCycle * 2):self.t + 1, 2])) else: del self.error_X[0] del self.error_Y[0] if self.t < 80: self.error_X.append( max(self.XLine.TimeAndX[ (self.t + 1 - QMCycle * 2):self.t + 1, 1]) - min(self.XLine.TimeAndX[ (self.t + 1 - QMCycle * 2):self.t + 1, 1])) self.error_Y.append( max(self.XLine.TimeAndX[ (self.t + 1 - QMCycle * 2):self.t + 1, 2]) - min(self.XLine.TimeAndX[ (self.t + 1 - QMCycle * 2):self.t + 1, 2])) else: self.error_X.append( max(self.XLine.TimeAndX[(80 - QMCycle * 2):80, 1]) - min(self.XLine.TimeAndX[(80 - QMCycle * 2):80, 1])) self.error_Y.append( max(self.XLine.TimeAndX[(80 - QMCycle * 2):80, 2]) - min(self.XLine.TimeAndX[(80 - QMCycle * 2):80, 2])) self.ui.errorDisplay_X.setText("X " + str(self.error_X)) self.ui.errorDisplay_Y.setText("Y " + str(self.error_Y)) self.AmplitudeSender.emit(self.error_X) self.XLine.update_figure() self.t += 1 def ChannelTest_slot(self, channelName): ''' 检测通道名是否有效 目前作废函数 :return: ''' if None == caget(channelName, timeout=1): # 尝试是否可以连接到通道并读取数据 QMessageBox.information(self, "提示", "无法连接到通道,请检查是否有误") def getChannelName(self): ''' 获取用户设置的通道名 :return: ''' global QMChannelName, DMChannelName lineEdit = self.sender() if lineEdit.objectName() == "BPMChannel_X": self.BPMChannelName_X = self.ui.BPMChannel_X.text() self.BPMChannelRight = False elif lineEdit.objectName() == "QMChannel": QMChannelName = self.ui.QMChannel.text() self.QMChannelRight = False elif lineEdit.objectName() == "DMChannel": DMChannelName = self.ui.DMChannel.text() self.DMChannelRight = False elif lineEdit.objectName() == "BPMChannel_Y": self.BPMChannelName_Y = self.ui.BPMChannel_Y.text() self.BPMChannelRight = False def QMCurrent_set(self): global QMInterval, QMCycle, QMChannelName, QMCurrentAmplitude if not self.QMChannelRight: if None == caget(QMChannelName, timeout=0.5): # 尝试是否可以连接到通道并读取数据 QMessageBox.information(self, "提示", "无法连接到通道,请检查是否有误!") else: self.QMChannelRight = True if self.QMChannelRight: if QMInterval == 0: caput(QMChannelName, QMCurrentAmplitude) else: # 为四极铁添加周期性电流(创建子线程文件) if self.sinCurrentRun: self.ui.QMPutBtn.setText("Put") self.sinCurrentRun = not self.sinCurrentRun # 线程停止并销毁 self.QMCurrent.stop() self.ui.QM_I.setReadOnly(False) else: self.ui.QMPutBtn.setText("Stop") self.sinCurrentRun = not self.sinCurrentRun #创建线程并开启 self.QMCurrent.start() self.QMSetParameters.emit(QMChannelName, QMCycle, QMInterval, QMCurrentAmplitude) self.ui.QM_I.setReadOnly(True) def DMCurrent_set(self): global DMInterval, DMChannelName, DMCurrentAmplitude if not self.DMChannelRight: if None == caget(DMChannelName, timeout=0.5): # 尝试是否可以连接到通道并读取数据 QMessageBox.information(self, "提示", "无法连接到通道,请检查是否有误!") else: self.DMChannelRight = True if self.DMChannelRight: if DMInterval == 0: caput(DMChannelName, DMCurrentAmplitude) else: if self.DMCurrentRun: self.ui.DMPutBtn.setText("Put") self.DMCurrentRun = not self.DMCurrentRun # 线程停止并销毁 self.DMCurrent.stop() self.ui.DM_I.setReadOnly(False) else: self.ui.DMPutBtn.setText("Stop") self.DMCurrentRun = not self.DMCurrentRun #创建线程并开启 self.DMCurrent = DMCurrent.DMCurrentThread( DMChannelName, DMInterval, DMCurrentAmplitude) self.DMCurrent.CurrentSignal.connect(self.receiveDMCurrent) self.QMCurrent.sendData.connect( self.DMCurrent.receiveError) self.DMCurrent.start() self.ui.DM_I.setReadOnly(True) def getFilePath(self): self.FilePath = QFileDialog.getSaveFileName( filter="Text Files (*.txt)") self.ui.FilePath.setText(self.FilePath[0]) if self.FilePath[0] == "": self.ui.StartBtn.setEnabled(False) else: self.ui.StartBtn.setEnabled(True) def getValue(self): global QMCurrentAmplitude, QMCycle, QMInterval, DMCurrentAmplitude, DMInterval SpinBox = self.sender() if SpinBox.objectName() == "QM_A": QMCurrentAmplitude = SpinBox.value() elif SpinBox.objectName() == "QM_C": QMCycle = SpinBox.value() elif SpinBox.objectName() == "QM_I": QMInterval = SpinBox.value() elif SpinBox.objectName() == "DM_C": DMCurrentAmplitude = SpinBox.value() elif SpinBox.objectName() == "DM_I": DMInterval = SpinBox.value() @pyqtSlot(float) def receiveDMCurrent(self, current): self.ui.DM_C.setValue(current)
class DiffView(QWidget): # {{{ SYNC_POSITION = 0.4 line_activated = pyqtSignal(object, object, object) def __init__(self, parent=None, show_open_in_editor=False): QWidget.__init__(self, parent) self.changes = [[], [], []] self.delta = 0 self.l = l = QHBoxLayout(self) self.setLayout(l) self.syncpos = 0 l.setContentsMargins(0, 0, 0, 0), l.setSpacing(0) self.view = DiffSplit(self, show_open_in_editor=show_open_in_editor) l.addWidget(self.view) self.add_diff = self.view.add_diff self.scrollbar = QScrollBar(self) l.addWidget(self.scrollbar) self.syncing = False self.bars = [] self.resize_timer = QTimer(self) self.resize_timer.setSingleShot(True) self.resize_timer.timeout.connect(self.resize_debounced) for i, bar in enumerate((self.scrollbar, self.view.left.verticalScrollBar(), self.view.right.verticalScrollBar())): self.bars.append(bar) bar.valueChanged[int].connect(partial(self.scrolled, i)) self.view.left.resized.connect(self.resized) for i, v in enumerate((self.view.left, self.view.right, self.view.handle(1))): v.wheel_event.connect(self.scrollbar.wheelEvent) if i < 2: v.next_change.connect(self.next_change) v.line_activated.connect(self.line_activated) v.scrolled.connect(partial(self.scrolled, i + 1)) def next_change(self, delta): assert delta in (1, -1) position = self.get_position_from_scrollbar(0) if position[0] == 'in': p = n = position[1] else: p, n = position[1], position[1] + 1 if p < 0: p = None if n >= len(self.changes[0]): n = None if p == n: nc = p + delta if nc < 0 or nc >= len(self.changes[0]): nc = None else: nc = {1:n, -1:p}[delta] if nc is None: self.scrollbar.setValue(0 if delta == -1 else self.scrollbar.maximum()) else: val = self.scrollbar.value() self.scroll_to(0, ('in', nc, 0)) nval = self.scrollbar.value() if nval == val: nval += 5 * delta if 0 <= nval <= self.scrollbar.maximum(): self.scrollbar.setValue(nval) def resized(self): self.resize_timer.start(300) def resize_debounced(self): self.view.resized() self.calculate_length() self.adjust_range() self.view.handle(1).update() def get_position_from_scrollbar(self, which): changes = self.changes[which] bar = self.bars[which] syncpos = self.syncpos + bar.value() prev = 0 for i, (top, bot, kind) in enumerate(changes): if syncpos <= bot: if top <= syncpos: # syncpos is inside a change try: ratio = float(syncpos - top) / (bot - top) except ZeroDivisionError: ratio = 0 return 'in', i, ratio else: # syncpos is after the previous change offset = syncpos - prev return 'after', i - 1, offset else: # syncpos is after the current change prev = bot offset = syncpos - prev return 'after', len(changes) - 1, offset def scroll_to(self, which, position): changes = self.changes[which] bar = self.bars[which] val = None if position[0] == 'in': change_idx, ratio = position[1:] start, end = changes[change_idx][:2] val = start + int((end - start) * ratio) else: change_idx, offset = position[1:] start = 0 if change_idx < 0 else changes[change_idx][1] val = start + offset bar.setValue(val - self.syncpos) def scrolled(self, which, *args): if self.syncing: return position = self.get_position_from_scrollbar(which) with self: for x in {0, 1, 2} - {which}: self.scroll_to(x, position) self.view.handle(1).update() def __enter__(self): self.syncing = True def __exit__(self, *args): self.syncing = False def clear(self): with self: self.view.clear() self.changes = [[], [], []] self.delta = 0 self.scrollbar.setRange(0, 0) def adjust_range(self): ls, rs = self.view.left.verticalScrollBar(), self.view.right.verticalScrollBar() self.scrollbar.setPageStep(min(ls.pageStep(), rs.pageStep())) self.scrollbar.setSingleStep(min(ls.singleStep(), rs.singleStep())) self.scrollbar.setRange(0, ls.maximum() + self.delta) self.scrollbar.setVisible(self.view.left.blockCount() > ls.pageStep() or self.view.right.blockCount() > rs.pageStep()) self.syncpos = int(ceil(self.scrollbar.pageStep() * self.SYNC_POSITION)) def finalize(self): self.view.finalize() self.changes = [[], [], []] self.calculate_length() self.adjust_range() def calculate_length(self): delta = 0 line_number_changes = ([], []) for v, lmap, changes in zip((self.view.left, self.view.right), ({}, {}), line_number_changes): b = v.document().firstBlock() ebl = v.document().documentLayout().ensureBlockLayout last_line_count = 0 while b.isValid(): ebl(b) lmap[b.blockNumber()] = last_line_count last_line_count += b.layout().lineCount() b = b.next() for top, bot, kind in v.changes: changes.append((lmap[top], lmap[bot], kind)) changes = [] for (l_top, l_bot, kind), (r_top, r_bot, kind) in zip(*line_number_changes): height = max(l_bot - l_top, r_bot - r_top) top = delta + l_top changes.append((top, top + height, kind)) delta = top + height - l_bot self.changes, self.delta = (changes,) + line_number_changes, delta def handle_key(self, ev): amount, d = None, 1 key = ev.key() if key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_J, Qt.Key_K): amount = self.scrollbar.singleStep() if key in (Qt.Key_Up, Qt.Key_K): d = -1 elif key in (Qt.Key_PageUp, Qt.Key_PageDown): amount = self.scrollbar.pageStep() if key in (Qt.Key_PageUp,): d = -1 elif key in (Qt.Key_Home, Qt.Key_End): self.scrollbar.setValue(0 if key == Qt.Key_Home else self.scrollbar.maximum()) return True elif key in (Qt.Key_N, Qt.Key_P): self.next_change(1 if key == Qt.Key_N else -1) return True if amount is not None: self.scrollbar.setValue(self.scrollbar.value() + d * amount) return True return False
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 bool(index.data(Qt.UserRole)) 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 EbookViewer(MainWindow): STATE_VERSION = 2 FLOW_MODE_TT = _('Switch to paged mode - where the text is broken up ' 'into pages like a paper book') PAGED_MODE_TT = _('Switch to flow mode - where the text is not broken up ' 'into pages') AUTOSAVE_INTERVAL = 10 # seconds msg_from_anotherinstance = pyqtSignal(object) def __init__(self, pathtoebook=None, debug_javascript=False, open_at=None, start_in_fullscreen=False, continue_reading=False, listener=None): MainWindow.__init__(self, debug_javascript) self.view.magnification_changed.connect(self.magnification_changed) self.closed = False self.show_toc_on_open = False self.listener = listener if listener is not None: t = Thread(name='ConnListener', target=listen, args=(self,)) t.daemon = True t.start() self.msg_from_anotherinstance.connect(self.another_instance_wants_to_talk, type=Qt.QueuedConnection) self.current_book_has_toc = False self.iterator = None self.current_page = None self.pending_search = None self.pending_search_dir= None self.pending_anchor = None self.pending_reference = None self.pending_bookmark = None self.pending_restore = False self.pending_goto_page = None self.cursor_hidden = False self.existing_bookmarks= [] self.selected_text = None self.was_maximized = False self.page_position_on_footnote_toggle = [] self.read_settings() self.autosave_timer = t = QTimer(self) t.setInterval(self.AUTOSAVE_INTERVAL * 1000), t.setSingleShot(True) t.timeout.connect(self.autosave) self.pos.value_changed.connect(self.update_pos_label) self.pos.value_changed.connect(self.autosave_timer.start) self.pos.setMinimumWidth(150) self.setFocusPolicy(Qt.StrongFocus) self.view.set_manager(self) self.pi = ProgressIndicator(self) self.action_quit = QAction(_('&Quit'), self) self.addAction(self.action_quit) self.view_resized_timer = QTimer(self) self.view_resized_timer.timeout.connect(self.viewport_resize_finished) self.view_resized_timer.setSingleShot(True) self.resize_in_progress = False self.action_reload = QAction(_('&Reload book'), self) self.action_reload.triggered.connect(self.reload_book) self.action_quit.triggered.connect(self.quit) self.action_reference_mode.triggered[bool].connect(self.view.reference_mode) self.action_metadata.triggered[bool].connect(self.metadata.setVisible) self.action_table_of_contents.toggled[bool].connect(self.set_toc_visible) self.action_copy.triggered[bool].connect(self.copy) self.action_font_size_larger.triggered.connect(self.font_size_larger) self.action_font_size_smaller.triggered.connect(self.font_size_smaller) self.action_open_ebook.triggered[bool].connect(self.open_ebook) self.action_next_page.triggered.connect(self.view.next_page) self.action_previous_page.triggered.connect(self.view.previous_page) self.action_find_next.triggered.connect(self.find_next) self.action_find_previous.triggered.connect(self.find_previous) self.action_full_screen.triggered[bool].connect(self.toggle_fullscreen) self.action_back.triggered[bool].connect(self.back) self.action_forward.triggered[bool].connect(self.forward) self.action_preferences.triggered.connect(self.do_config) self.pos.editingFinished.connect(self.goto_page_num) self.vertical_scrollbar.valueChanged[int].connect(lambda x:self.goto_page(x/100.)) self.search.search.connect(self.find) self.search.focus_to_library.connect(lambda: self.view.setFocus(Qt.OtherFocusReason)) self.toc.pressed[QModelIndex].connect(self.toc_clicked) self.toc.searched.connect(partial(self.toc_clicked, force=True)) def toggle_toc(ev): try: key = self.view.shortcuts.get_match(ev) except AttributeError: pass if key == 'Table of Contents': ev.accept() self.action_table_of_contents.trigger() return True return False self.toc.handle_shortcuts = toggle_toc self.reference.goto.connect(self.goto) self.bookmarks.edited.connect(self.bookmarks_edited) self.bookmarks.activated.connect(self.goto_bookmark) self.bookmarks.create_requested.connect(self.bookmark) self.set_bookmarks([]) self.load_theme_menu() if pathtoebook is not None: f = functools.partial(self.load_ebook, pathtoebook, open_at=open_at) QTimer.singleShot(50, f) elif continue_reading: QTimer.singleShot(50, self.continue_reading) self.window_mode_changed = None self.toggle_toolbar_action = QAction(_('Show/hide controls'), self) self.toggle_toolbar_action.setCheckable(True) self.toggle_toolbar_action.triggered.connect(self.toggle_toolbars) self.toolbar_hidden = None self.addAction(self.toggle_toolbar_action) self.full_screen_label_anim = QPropertyAnimation( self.full_screen_label, b'size') self.clock_timer = QTimer(self) self.clock_timer.timeout.connect(self.update_clock) self.action_print.triggered.connect(self.print_book) self.clear_recent_history_action = QAction( _('Clear list of recently opened books'), self) self.clear_recent_history_action.triggered.connect(self.clear_recent_history) self.build_recent_menu() self.open_history_menu.triggered.connect(self.open_recent) for x in ('tool_bar', 'tool_bar2'): x = getattr(self, x) for action in x.actions(): # So that the keyboard shortcuts for these actions will # continue to function even when the toolbars are hidden self.addAction(action) for plugin in self.view.document.all_viewer_plugins: plugin.customize_ui(self) self.view.document.settings_changed.connect(self.settings_changed) self.restore_state() self.settings_changed() self.action_toggle_paged_mode.toggled[bool].connect(self.toggle_paged_mode) if (start_in_fullscreen or self.view.document.start_in_fullscreen): self.action_full_screen.trigger() self.hide_cursor_timer = t = QTimer(self) t.setSingleShot(True), t.setInterval(3000) t.timeout.connect(self.hide_cursor) t.start() def eventFilter(self, obj, ev): if ev.type() == ev.MouseMove: if self.cursor_hidden: self.cursor_hidden = False QApplication.instance().restoreOverrideCursor() self.hide_cursor_timer.start() return False def hide_cursor(self): self.cursor_hidden = True QApplication.instance().setOverrideCursor(Qt.BlankCursor) def toggle_paged_mode(self, checked, at_start=False): in_paged_mode = not self.action_toggle_paged_mode.isChecked() self.view.document.in_paged_mode = in_paged_mode self.action_toggle_paged_mode.setToolTip(self.FLOW_MODE_TT if self.action_toggle_paged_mode.isChecked() else self.PAGED_MODE_TT) if at_start: return self.reload() def settings_changed(self): for x in ('', '2'): x = getattr(self, 'tool_bar'+x) x.setVisible(self.view.document.show_controls) def reload(self): if hasattr(self, 'current_index') and self.current_index > -1: self.view.document.page_position.save(overwrite=False) self.pending_restore = True self.load_path(self.view.last_loaded_path) def set_toc_visible(self, yes): self.toc_dock.setVisible(yes) if not yes: self.show_toc_on_open = False def clear_recent_history(self, *args): vprefs.set('viewer_open_history', []) self.build_recent_menu() def build_recent_menu(self): m = self.open_history_menu m.clear() recent = vprefs.get('viewer_open_history', []) if recent: m.addAction(self.clear_recent_history_action) m.addSeparator() count = 0 for path in recent: if count > 9: break if os.path.exists(path): m.addAction(RecentAction(path, m)) count += 1 def continue_reading(self): actions = self.open_history_menu.actions()[2:] if actions: actions[0].trigger() def shutdown(self): if self.isFullScreen() and not self.view.document.start_in_fullscreen: self.action_full_screen.trigger() return False self.save_state() if self.listener is not None: self.listener.close() return True def quit(self): if self.shutdown(): QApplication.instance().quit() def closeEvent(self, e): if self.closed: e.ignore() return if self.shutdown(): self.closed = True return MainWindow.closeEvent(self, e) else: e.ignore() def toggle_toolbars(self): for x in ('tool_bar', 'tool_bar2'): x = getattr(self, x) x.setVisible(not x.isVisible()) def save_state(self): state = bytearray(self.saveState(self.STATE_VERSION)) vprefs['main_window_state'] = state if not self.isFullScreen(): vprefs.set('viewer_window_geometry', bytearray(self.saveGeometry())) if self.current_book_has_toc: vprefs.set('viewer_toc_isvisible', self.show_toc_on_open or bool(self.toc_dock.isVisible())) vprefs['multiplier'] = self.view.multiplier vprefs['in_paged_mode'] = not self.action_toggle_paged_mode.isChecked() def restore_state(self): state = vprefs.get('main_window_state', None) if state is not None: try: state = QByteArray(state) self.restoreState(state, self.STATE_VERSION) except: pass self.initialize_dock_state() mult = vprefs.get('multiplier', None) if mult: self.view.multiplier = mult # On windows Qt lets the user hide toolbars via a right click in a very # specific location, ensure they are visible. self.tool_bar.setVisible(True) self.tool_bar2.setVisible(True) self.toc_dock.close() # This will be opened on book open, if the book has a toc and it was previously opened self.action_toggle_paged_mode.setChecked(not vprefs.get('in_paged_mode', True)) self.toggle_paged_mode(self.action_toggle_paged_mode.isChecked(), at_start=True) def lookup(self, word): from urllib import quote word = quote(word.encode('utf-8')) try: url = lookup_website(canonicalize_lang(self.view.current_language) or 'en').format(word=word) except Exception: traceback.print_exc() url = default_lookup_website(canonicalize_lang(self.view.current_language) or 'en').format(word=word) open_url(url) def print_book(self): if self.iterator is None: return error_dialog(self, _('No book opened'), _( 'Cannot print as no book is opened'), show=True) from calibre.gui2.viewer.printing import print_book print_book(self.iterator.pathtoebook, self, self.current_title) def toggle_fullscreen(self): if self.isFullScreen(): self.showNormal() else: self.showFullScreen() def showFullScreen(self): self.view.document.page_position.save() self.window_mode_changed = 'fullscreen' self.tool_bar.setVisible(False) self.tool_bar2.setVisible(False) self.was_maximized = self.isMaximized() if not self.view.document.fullscreen_scrollbar: self.vertical_scrollbar.setVisible(False) super(EbookViewer, self).showFullScreen() def show_full_screen_label(self): f = self.full_screen_label height = f.final_height width = int(0.7*self.view.width()) f.resize(width, height) if self.view.document.show_fullscreen_help: f.setVisible(True) a = self.full_screen_label_anim a.setDuration(500) a.setStartValue(QSize(width, 0)) a.setEndValue(QSize(width, height)) a.start() QTimer.singleShot(3500, self.full_screen_label.hide) if self.view.document.fullscreen_clock: self.show_clock() if self.view.document.fullscreen_pos: self.show_pos_label() self.relayout_fullscreen_labels() def show_clock(self): self.clock_label.setVisible(True) self.clock_label.setText(QTime(22, 33, 33).toString(Qt.SystemLocaleShortDate)) self.clock_timer.start(1000) self.clock_label.setStyleSheet(self.info_label_style%( 'rgba(0, 0, 0, 0)', self.view.document.colors()[1])) self.clock_label.resize(self.clock_label.sizeHint()) self.update_clock() def show_pos_label(self): self.pos_label.setVisible(True) self.pos_label.setStyleSheet(self.info_label_style%( 'rgba(0, 0, 0, 0)', self.view.document.colors()[1])) self.update_pos_label() def relayout_fullscreen_labels(self): vswidth = (self.vertical_scrollbar.width() if self.vertical_scrollbar.isVisible() else 0) p = self.pos_label p.move(15, p.parent().height() - p.height()-10) c = self.clock_label c.move(c.parent().width() - vswidth - 15 - c.width(), c.parent().height() - c.height() - 10) f = self.full_screen_label f.move((f.parent().width() - f.width())//2, (f.parent().height() - f.final_height)//2) def update_clock(self): self.clock_label.setText(QTime.currentTime().toString(Qt.SystemLocaleShortDate)) def update_pos_label(self, *args): if self.pos_label.isVisible(): try: value, maximum = args except: value, maximum = self.pos.value(), self.pos.maximum() text = '%g/%g'%(value, maximum) self.pos_label.setText(text) self.pos_label.resize(self.pos_label.sizeHint()) def showNormal(self): self.view.document.page_position.save() self.clock_label.setVisible(False) self.pos_label.setVisible(False) self.clock_timer.stop() self.vertical_scrollbar.setVisible(True) self.window_mode_changed = 'normal' self.settings_changed() self.full_screen_label.setVisible(False) if self.was_maximized: super(EbookViewer, self).showMaximized() else: super(EbookViewer, self).showNormal() def goto(self, ref): if ref: tokens = ref.split('.') if len(tokens) > 1: spine_index = int(tokens[0]) -1 if spine_index == self.current_index: self.view.goto(ref) else: self.pending_reference = ref self.load_path(self.iterator.spine[spine_index]) def goto_bookmark(self, bm): spine_index = bm['spine'] if spine_index > -1 and self.current_index == spine_index: if self.resize_in_progress: self.view.document.page_position.set_pos(bm['pos']) else: self.view.goto_bookmark(bm) # Going to a bookmark does not call scrolled() so we update the # page position explicitly. Use a timer to ensure it is # accurate. QTimer.singleShot(100, self.update_page_number) else: self.pending_bookmark = bm if spine_index < 0 or spine_index >= len(self.iterator.spine): spine_index = 0 self.pending_bookmark = None self.load_path(self.iterator.spine[spine_index]) def toc_clicked(self, index, force=False): if force or QApplication.mouseButtons() & Qt.LeftButton: item = self.toc_model.itemFromIndex(index) if item.abspath is not None: if not os.path.exists(item.abspath): return error_dialog(self, _('No such location'), _('The location pointed to by this item' ' does not exist.'), det_msg=item.abspath, show=True) url = QUrl.fromLocalFile(item.abspath) if item.fragment: url.setFragment(item.fragment) self.link_clicked(url) self.view.setFocus(Qt.OtherFocusReason) def selection_changed(self, selected_text): self.selected_text = selected_text.strip() self.action_copy.setEnabled(bool(self.selected_text)) def copy(self, x): if self.selected_text: QApplication.clipboard().setText(self.selected_text) def back(self, x): pos = self.history.back(self.pos.value()) if pos is not None: self.goto_page(pos) def goto_page_num(self): num = self.pos.value() self.goto_page(num) def forward(self, x): pos = self.history.forward(self.pos.value()) if pos is not None: self.goto_page(pos) def goto_start(self): self.goto_page(1) def goto_end(self): self.goto_page(self.pos.maximum()) def goto_page(self, new_page, loaded_check=True): if self.current_page is not None or not loaded_check: for page in self.iterator.spine: if new_page >= page.start_page and new_page <= page.max_page: try: frac = float(new_page-page.start_page)/(page.pages-1) except ZeroDivisionError: frac = 0 if page == self.current_page: self.view.scroll_to(frac) else: self.load_path(page, pos=frac) def open_ebook(self, checked): files = choose_files(self, 'ebook viewer open dialog', _('Choose ebook'), [(_('Ebooks'), available_input_formats())], all_files=False, select_only_single_file=True) if files: self.load_ebook(files[0]) def open_recent(self, action): self.load_ebook(action.path) def font_size_larger(self): self.view.magnify_fonts() def font_size_smaller(self): self.view.shrink_fonts() def magnification_changed(self, val): tt = '%(action)s [%(sc)s]\n'+_('Current magnification: %(mag).1f') sc = _(' or ').join(self.view.shortcuts.get_shortcuts('Font larger')) self.action_font_size_larger.setToolTip( tt %dict(action=unicode(self.action_font_size_larger.text()), mag=val, sc=sc)) sc = _(' or ').join(self.view.shortcuts.get_shortcuts('Font smaller')) self.action_font_size_smaller.setToolTip( tt %dict(action=unicode(self.action_font_size_smaller.text()), mag=val, sc=sc)) self.action_font_size_larger.setEnabled(self.view.multiplier < 3) self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2) def find(self, text, repeat=False, backwards=False): if not text: self.view.search('') return self.search.search_done(False) if self.view.search(text, backwards=backwards): self.scrolled(self.view.scroll_fraction) return self.search.search_done(True) index = self.iterator.search(text, self.current_index, backwards=backwards) if index is None: if self.current_index > 0: index = self.iterator.search(text, 0) if index is None: info_dialog(self, _('No matches found'), _('No matches found for: %s')%text).exec_() return self.search.search_done(True) return self.search.search_done(True) self.pending_search = text self.pending_search_dir = 'backwards' if backwards else 'forwards' self.load_path(self.iterator.spine[index]) def find_next(self): self.find(unicode(self.search.text()), repeat=True) def find_previous(self): self.find(unicode(self.search.text()), repeat=True, backwards=True) def do_search(self, text, backwards): self.pending_search = None self.pending_search_dir = None if self.view.search(text, backwards=backwards): self.scrolled(self.view.scroll_fraction) def internal_link_clicked(self, prev_pos): self.history.add(prev_pos) def link_clicked(self, url): path = os.path.abspath(unicode(url.toLocalFile())) frag = None if path in self.iterator.spine: self.update_page_number() # Ensure page number is accurate as it is used for history self.history.add(self.pos.value()) path = self.iterator.spine[self.iterator.spine.index(path)] if url.hasFragment(): frag = unicode(url.fragment()) if path != self.current_page: self.pending_anchor = frag self.load_path(path) else: oldpos = self.view.document.ypos if frag: self.view.scroll_to(frag) else: # Scroll to top self.view.scroll_to(0) if self.view.document.ypos == oldpos: # If we are coming from goto_next_section() call this will # cause another goto next section call with the next toc # entry, since this one did not cause any scrolling at all. QTimer.singleShot(10, self.update_indexing_state) else: open_url(url) def load_started(self): self.open_progress_indicator(_('Loading flow...')) def load_finished(self, ok): self.close_progress_indicator() path = self.view.path() try: index = self.iterator.spine.index(path) except (ValueError, AttributeError): return -1 self.current_page = self.iterator.spine[index] self.current_index = index self.set_page_number(self.view.scroll_fraction) QTimer.singleShot(100, self.update_indexing_state) if self.pending_search is not None: self.do_search(self.pending_search, self.pending_search_dir=='backwards') self.pending_search = None self.pending_search_dir = None if self.pending_anchor is not None: self.view.scroll_to(self.pending_anchor) self.pending_anchor = None if self.pending_reference is not None: self.view.goto(self.pending_reference) self.pending_reference = None if self.pending_bookmark is not None: self.goto_bookmark(self.pending_bookmark) self.pending_bookmark = None if self.pending_restore: self.view.document.page_position.restore() return self.current_index def goto_next_section(self): if hasattr(self, 'current_index'): entry = self.toc_model.next_entry(self.current_index, self.view.document.read_anchor_positions(), self.view.viewport_rect, self.view.document.in_paged_mode) if entry is not None: self.pending_goto_next_section = ( self.toc_model.currently_viewed_entry, entry, False) self.toc_clicked(entry.index(), force=True) def goto_previous_section(self): if hasattr(self, 'current_index'): entry = self.toc_model.next_entry(self.current_index, self.view.document.read_anchor_positions(), self.view.viewport_rect, self.view.document.in_paged_mode, backwards=True) if entry is not None: self.pending_goto_next_section = ( self.toc_model.currently_viewed_entry, entry, True) self.toc_clicked(entry.index(), force=True) def update_indexing_state(self, anchor_positions=None): pgns = getattr(self, 'pending_goto_next_section', None) if hasattr(self, 'current_index'): if anchor_positions is None: anchor_positions = self.view.document.read_anchor_positions() items = self.toc_model.update_indexing_state(self.current_index, self.view.viewport_rect, anchor_positions, self.view.document.in_paged_mode) if items: self.toc.scrollTo(items[-1].index()) if pgns is not None: self.pending_goto_next_section = None # Check that we actually progressed if pgns[0] is self.toc_model.currently_viewed_entry: entry = self.toc_model.next_entry(self.current_index, self.view.document.read_anchor_positions(), self.view.viewport_rect, self.view.document.in_paged_mode, backwards=pgns[2], current_entry=pgns[1]) if entry is not None: self.pending_goto_next_section = ( self.toc_model.currently_viewed_entry, entry, pgns[2]) self.toc_clicked(entry.index(), force=True) def load_path(self, path, pos=0.0): self.open_progress_indicator(_('Laying out %s')%self.current_title) self.view.load_path(path, pos=pos) def footnote_visibility_changed(self, is_visible): if self.view.document.in_paged_mode: pp = namedtuple('PagePosition', 'time is_visible page_dimensions multiplier last_loaded_path page_number after_resize_page_number') self.page_position_on_footnote_toggle.append(pp( time.time(), is_visible, self.view.document.page_dimensions, self.view.multiplier, self.view.last_loaded_path, self.view.document.page_number, None)) def pre_footnote_toggle_position(self): num = len(self.page_position_on_footnote_toggle) if self.view.document.in_paged_mode and num > 1 and num % 2 == 0: two, one = self.page_position_on_footnote_toggle.pop(), self.page_position_on_footnote_toggle.pop() if ( time.time() - two.time < 1 and not two.is_visible and one.is_visible and one.last_loaded_path == two.last_loaded_path and two.last_loaded_path == self.view.last_loaded_path and one.page_dimensions == self.view.document.page_dimensions and one.multiplier == self.view.multiplier and one.after_resize_page_number == self.view.document.page_number ): return one.page_number def viewport_resize_started(self, event): if not self.resize_in_progress: # First resize, so save the current page position self.resize_in_progress = True if not self.window_mode_changed: # The special handling for window mode changed will already # have saved page position, so only save it if this is not a # mode change self.view.document.page_position.save() if self.resize_in_progress: self.view_resized_timer.start(75) def viewport_resize_finished(self): # There hasn't been a resize event for some time # restore the current page position. self.resize_in_progress = False wmc, self.window_mode_changed = self.window_mode_changed, None fs = wmc == 'fullscreen' if wmc: # Sets up body text margins, which can be limited in fs mode by a # separate config option, so must be done before relayout of text (self.view.document.switch_to_fullscreen_mode if fs else self.view.document.switch_to_window_mode)() # Re-layout text, must be done before restoring page position self.view.document.after_resize() if wmc: # This resize is part of a window mode change, special case it if fs: self.show_full_screen_label() self.view.document.page_position.restore() self.scrolled(self.view.scroll_fraction) else: if self.isFullScreen(): self.relayout_fullscreen_labels() pre_footnote_pos = self.pre_footnote_toggle_position() if pre_footnote_pos is not None: self.view.document.page_number = pre_footnote_pos else: self.view.document.page_position.restore() self.update_page_number() if len(self.page_position_on_footnote_toggle) % 2 == 1: self.page_position_on_footnote_toggle[-1] = self.page_position_on_footnote_toggle[-1]._replace( after_resize_page_number=self.view.document.page_number) if self.pending_goto_page is not None: pos, self.pending_goto_page = self.pending_goto_page, None self.goto_page(pos, loaded_check=False) def update_page_number(self): self.set_page_number(self.view.document.scroll_fraction) return self.pos.value() def close_progress_indicator(self): self.pi.stop() for o in ('tool_bar', 'tool_bar2', 'view', 'horizontal_scrollbar', 'vertical_scrollbar'): getattr(self, o).setEnabled(True) self.unsetCursor() self.view.setFocus(Qt.PopupFocusReason) def open_progress_indicator(self, msg=''): self.pi.start(msg) for o in ('tool_bar', 'tool_bar2', 'view', 'horizontal_scrollbar', 'vertical_scrollbar'): getattr(self, o).setEnabled(False) self.setCursor(Qt.BusyCursor) def load_theme_menu(self): from calibre.gui2.viewer.config import load_themes self.themes_menu.clear() for key in load_themes(): title = key[len('theme_'):] self.themes_menu.addAction(title, partial(self.load_theme, key)) def load_theme(self, theme_id): self.view.load_theme(theme_id) def do_config(self): self.view.config(self) self.load_theme_menu() if self.iterator is not None: self.iterator.copy_bookmarks_to_file = self.view.document.copy_bookmarks_to_file from calibre.gui2 import config if not config['viewer_search_history']: self.search.clear_history() def bookmark(self, *args): num = 1 bm = None while True: bm = _('Bookmark #%d')%num if bm not in self.existing_bookmarks: break num += 1 title, ok = QInputDialog.getText(self, _('Add bookmark'), _('Enter title for bookmark:'), text=bm) title = unicode(title).strip() if ok and title: bm = self.view.bookmark() bm['spine'] = self.current_index bm['title'] = title self.iterator.add_bookmark(bm) self.set_bookmarks(self.iterator.bookmarks) self.bookmarks.set_current_bookmark(bm) def autosave(self): self.save_current_position(no_copy_to_file=True) def bookmarks_edited(self, bookmarks): self.build_bookmarks_menu(bookmarks) self.iterator.set_bookmarks(bookmarks) self.iterator.save_bookmarks() def build_bookmarks_menu(self, bookmarks): self.bookmarks_menu.clear() sc = _(' or ').join(self.view.shortcuts.get_shortcuts('Bookmark')) self.bookmarks_menu.addAction(_("Bookmark this location [%s]") % sc, self.bookmark) self.bookmarks_menu.addAction(_("Show/hide Bookmarks"), self.bookmarks_dock.toggleViewAction().trigger) self.bookmarks_menu.addSeparator() current_page = None self.existing_bookmarks = [] for bm in bookmarks: if bm['title'] == 'calibre_current_page_bookmark': if self.view.document.remember_current_page: current_page = bm else: self.existing_bookmarks.append(bm['title']) self.bookmarks_menu.addAction(bm['title'], partial(self.goto_bookmark, bm)) return current_page def set_bookmarks(self, bookmarks): self.bookmarks.set_bookmarks(bookmarks) return self.build_bookmarks_menu(bookmarks) @property def current_page_bookmark(self): bm = self.view.bookmark() bm['spine'] = self.current_index bm['title'] = 'calibre_current_page_bookmark' return bm def save_current_position(self, no_copy_to_file=False): if not self.view.document.remember_current_page: return if hasattr(self, 'current_index'): try: self.iterator.add_bookmark(self.current_page_bookmark, no_copy_to_file=no_copy_to_file) except: traceback.print_exc() def another_instance_wants_to_talk(self, msg): try: path, open_at = msg except Exception: return self.load_ebook(path, open_at=open_at) self.raise_() def load_ebook(self, pathtoebook, open_at=None, reopen_at=None): if self.iterator is not None: self.save_current_position() self.iterator.__exit__() self.iterator = EbookIterator(pathtoebook, copy_bookmarks_to_file=self.view.document.copy_bookmarks_to_file) self.history.clear() self.open_progress_indicator(_('Loading ebook...')) worker = Worker(target=partial(self.iterator.__enter__, view_kepub=True)) worker.path_to_ebook = pathtoebook worker.start() while worker.isAlive(): worker.join(0.1) QApplication.processEvents() if worker.exception is not None: tb = worker.traceback.strip() if tb and tb.splitlines()[-1].startswith('DRMError:'): from calibre.gui2.dialogs.drm_error import DRMErrorMessage DRMErrorMessage(self).exec_() else: r = getattr(worker.exception, 'reason', worker.exception) error_dialog(self, _('Could not open ebook'), as_unicode(r) or _('Unknown error'), det_msg=tb, show=True) self.close_progress_indicator() else: self.metadata.show_opf(self.iterator.opf, self.iterator.book_format) self.view.current_language = self.iterator.language title = self.iterator.opf.title if not title: title = os.path.splitext(os.path.basename(pathtoebook))[0] if self.iterator.toc: self.toc_model = TOC(self.iterator.spine, self.iterator.toc) self.toc.setModel(self.toc_model) if self.show_toc_on_open: self.action_table_of_contents.setChecked(True) else: self.toc_model = TOC(self.iterator.spine) self.toc.setModel(self.toc_model) self.action_table_of_contents.setChecked(False) if isbytestring(pathtoebook): pathtoebook = force_unicode(pathtoebook, filesystem_encoding) vh = vprefs.get('viewer_open_history', []) try: vh.remove(pathtoebook) except: pass vh.insert(0, pathtoebook) vprefs.set('viewer_open_history', vh[:50]) self.build_recent_menu() self.footnotes_dock.close() self.action_table_of_contents.setDisabled(not self.iterator.toc) self.current_book_has_toc = bool(self.iterator.toc) self.current_title = title self.setWindowTitle(title + ' [%s]'%self.iterator.book_format + ' - ' + self.base_window_title) self.pos.setMaximum(sum(self.iterator.pages)) self.pos.setSuffix(' / %d'%sum(self.iterator.pages)) self.vertical_scrollbar.setMinimum(100) self.vertical_scrollbar.setMaximum(100*sum(self.iterator.pages)) self.vertical_scrollbar.setSingleStep(10) self.vertical_scrollbar.setPageStep(100) self.set_vscrollbar_value(1) self.current_index = -1 QApplication.instance().alert(self, 5000) previous = self.set_bookmarks(self.iterator.bookmarks) if reopen_at is not None: previous = reopen_at if open_at is None and previous is not None: self.goto_bookmark(previous) else: if open_at is None: self.next_document() else: if open_at > self.pos.maximum(): open_at = self.pos.maximum() if open_at < self.pos.minimum(): open_at = self.pos.minimum() if self.resize_in_progress: self.pending_goto_page = open_at else: self.goto_page(open_at, loaded_check=False) def set_vscrollbar_value(self, pagenum): self.vertical_scrollbar.blockSignals(True) self.vertical_scrollbar.setValue(int(pagenum*100)) self.vertical_scrollbar.blockSignals(False) def set_page_number(self, frac): if getattr(self, 'current_page', None) is not None: page = self.current_page.start_page + frac*float(self.current_page.pages-1) self.pos.set_value(page) self.set_vscrollbar_value(page) def scrolled(self, frac, onload=False): self.set_page_number(frac) if not onload: ap = self.view.document.read_anchor_positions() self.update_indexing_state(ap) def next_document(self): if (hasattr(self, 'current_index') and self.current_index < len(self.iterator.spine) - 1): self.load_path(self.iterator.spine[self.current_index+1]) def previous_document(self): if hasattr(self, 'current_index') and self.current_index > 0: self.load_path(self.iterator.spine[self.current_index-1], pos=1.0) def keyPressEvent(self, event): if event.key() == Qt.Key_Escape: if self.metadata.isVisible(): self.metadata.setVisible(False) event.accept() return if self.isFullScreen(): self.action_full_screen.trigger() event.accept() return try: key = self.view.shortcuts.get_match(event) except AttributeError: return MainWindow.keyPressEvent(self, event) try: bac = self.bookmarks_menu.actions()[0] except (AttributeError, TypeError, IndexError, KeyError): bac = None action = { 'Quit':self.action_quit, 'Show metadata':self.action_metadata, 'Copy':self.view.copy_action, 'Font larger': self.action_font_size_larger, 'Font smaller': self.action_font_size_smaller, 'Fullscreen': self.action_full_screen, 'Find next': self.action_find_next, 'Find previous': self.action_find_previous, 'Search online': self.view.search_online_action, 'Lookup word': self.view.dictionary_action, 'Next occurrence': self.view.search_action, 'Bookmark': bac, 'Reload': self.action_reload, 'Table of Contents': self.action_table_of_contents, 'Print': self.action_print, }.get(key, None) if action is not None: event.accept() action.trigger() return if key == 'Focus Search': self.search.setFocus(Qt.OtherFocusReason) return if not self.view.handle_key_press(event): event.ignore() def reload_book(self): if getattr(self.iterator, 'pathtoebook', None): try: reopen_at = self.current_page_bookmark except Exception: reopen_at = None self.history.clear() self.load_ebook(self.iterator.pathtoebook, reopen_at=reopen_at) return def __enter__(self): return self def __exit__(self, *args): if self.iterator is not None: self.save_current_position() self.iterator.__exit__(*args) def read_settings(self): c = config().parse() if c.remember_window_size: wg = vprefs.get('viewer_window_geometry', None) if wg is not None: self.restoreGeometry(wg) self.show_toc_on_open = vprefs.get('viewer_toc_isvisible', False) desktop = QApplication.instance().desktop() av = desktop.availableGeometry(self).height() - 30 if self.height() > av: self.resize(self.width(), av) def show_footnote_view(self): self.footnotes_dock.show()
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 report_worker_launch_error(self): if parse_worker.launch_error is not None: tb, parse_worker.launch_error = parse_worker.launch_error, None error_dialog(self, _('Failed to launch worker'), _( 'Failed to launch the worker process used for rendering the preview'), det_msg=tb, show=True) def show(self, name): if name != self.current_name: self.refresh_timer.stop() self.current_name = name self.report_worker_launch_error() 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 self.report_worker_launch_error() 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 MainWindow(QWidget): def __init__(self): super().__init__() title = Path(__file__).parent.name self.setWindowTitle(title) timeout = 1000 // 60 # Таймер обновления движения и обработки столкновения шариков self.timer = QTimer() self.timer.timeout.connect(self.tick) self.timer.start(timeout) # NOTE: Перерисовку помести в tick, но этот вариант с отдельным таймером тоже # имеет право быть # # Таймер перерисовки окна # self.timer_render = QTimer() # self.timer_render.timeout.connect(self.update) # self.timer_render.start(timeout) self.outer_circle = 195 self.ball = Ball() self.mouse_center_x = 0 self.mouse_center_y = 0 # Используется, чтобы в независимости от количества вызовов # tick скорость шарика была одинаковая self.t = 0 self.setMouseTracking(True) def tick(self): # Считаем сколько времени прошло с прошлого обсчета dt = default_timer() - self.t self.ball.hit_mouse_check(self.mouse_center_x, self.mouse_center_y) self.ball.do_move(dt) self.ball.hit_outer_circle_check(self.outer_circle) self.t = default_timer() self.update() def mouseMoveEvent(self, event): self.mouse_center_x = event.pos().x() - (self.width() / 2) self.mouse_center_y = event.pos().y() - (self.height() / 2) def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.HighQualityAntialiasing) painter.setPen(QPen(Qt.red, 3)) painter.setBrush(Qt.transparent) painter.translate(self.width() / 2, self.height() / 2) painter.drawEllipse(QPoint(0, 0), self.outer_circle, self.outer_circle) painter.drawEllipse(QPoint(self.ball.x, self.ball.y), self.ball.r, self.ball.r)
class SearchBox2(QComboBox): # {{{ ''' To use this class: * Call initialize() * Connect to the search() and cleared() signals from this widget. * Connect to the changed() signal to know when the box content changes * Connect to focus_to_library() signal to be told to manually change focus * Call search_done() after every search is complete * Call set_search_string() to perform a search programmatically * You can use the current_text property to get the current search text Be aware that if you are using it in a slot connected to the changed() signal, if the connection is not queued it will not be accurate. ''' INTERVAL = 1500 #: Time to wait before emitting search signal MAX_COUNT = 25 search = pyqtSignal(object) cleared = pyqtSignal() changed = pyqtSignal() focus_to_library = pyqtSignal() def __init__(self, parent=None, add_clear_action=True): QComboBox.__init__(self, parent) self.line_edit = SearchLineEdit(self) self.setLineEdit(self.line_edit) self.line_edit.clear_history.connect(self.clear_history) if add_clear_action: self.lineEdit().setClearButtonEnabled(True) ac = self.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) if ac is not None: ac.triggered.connect(self.clear_clicked) c = self.line_edit.completer() c.setCompletionMode(c.PopupCompletion) c.highlighted[native_string_type].connect(self.completer_used) self.line_edit.key_pressed.connect(self.key_pressed, type=Qt.DirectConnection) # QueuedConnection as workaround for https://bugreports.qt-project.org/browse/QTBUG-40807 self.activated[native_string_type].connect(self.history_selected, type=Qt.QueuedConnection) self.setEditable(True) self.as_you_type = True self.timer = QTimer() self.timer.setSingleShot(True) self.timer.timeout.connect(self.timer_event, type=Qt.QueuedConnection) self.setInsertPolicy(self.NoInsert) self.setMaxCount(self.MAX_COUNT) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setMinimumContentsLength(25) self._in_a_search = False self.tool_tip_text = self.toolTip() def add_action(self, icon, position=QLineEdit.TrailingPosition): if not isinstance(icon, QIcon): icon = QIcon(I(icon)) return self.lineEdit().addAction(icon, position) def initialize(self, opt_name, colorize=False, help_text=_('Search'), as_you_type=None): self.as_you_type = config[ 'search_as_you_type'] if as_you_type is None else as_you_type self.opt_name = opt_name items = [] for item in config[opt_name]: if item not in items: items.append(item) self.addItems(items) self.line_edit.setPlaceholderText(help_text) self.colorize = colorize self.clear() def clear_search_history(self): config[self.opt_name] = [] self.clear() def hide_completer_popup(self): try: self.lineEdit().completer().popup().setVisible(False) except: pass def normalize_state(self): self.setToolTip(self.tool_tip_text) self.line_edit.setStyleSheet('') def text(self): return self.currentText() def clear_history(self, *args): QComboBox.clear(self) def clear(self, emit_search=True): self.normalize_state() self.setEditText('') if emit_search: self.search.emit('') self._in_a_search = False self.cleared.emit() def clear_clicked(self, *args): self.clear() self.setFocus(Qt.OtherFocusReason) def search_done(self, ok): if isinstance(ok, string_or_bytes): self.setToolTip(ok) ok = False if not unicode_type(self.currentText()).strip(): self.clear(emit_search=False) return self._in_a_search = ok if self.colorize: self.line_edit.setStyleSheet( QApplication.instance().stylesheet_for_line_edit(not ok)) else: self.line_edit.setStyleSheet('') # Comes from the lineEdit control def key_pressed(self, event): k = event.key() if k in (Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End, Qt.Key_PageUp, Qt.Key_PageDown, Qt.Key_unknown): return self.normalize_state() if self._in_a_search: self.changed.emit() self._in_a_search = False if event.key() in (Qt.Key_Return, Qt.Key_Enter): self.do_search() self.focus_to_library.emit() elif self.as_you_type and unicode_type(event.text()): self.timer.start(1500) # Comes from the combobox itself def keyPressEvent(self, event): k = event.key() if k in (Qt.Key_Enter, Qt.Key_Return): return self.do_search() if k not in (Qt.Key_Up, Qt.Key_Down): return QComboBox.keyPressEvent(self, event) self.blockSignals(True) self.normalize_state() if k == Qt.Key_Down and self.currentIndex( ) == 0 and not self.lineEdit().text(): self.setCurrentIndex(1), self.setCurrentIndex(0) event.accept() else: QComboBox.keyPressEvent(self, event) self.blockSignals(False) def completer_used(self, text): self.timer.stop() self.normalize_state() def timer_event(self): self._do_search(as_you_type=True) def history_selected(self, text): self.changed.emit() self.do_search() def _do_search(self, store_in_history=True, as_you_type=False): self.hide_completer_popup() text = unicode_type(self.currentText()).strip() if not text: return self.clear() if as_you_type: text = AsYouType(text) self.search.emit(text) if store_in_history: idx = self.findText(text, Qt.MatchFixedString | Qt.MatchCaseSensitive) self.block_signals(True) if idx < 0: self.insertItem(0, text) else: t = self.itemText(idx) self.removeItem(idx) self.insertItem(0, t) self.setCurrentIndex(0) self.block_signals(False) history = [ unicode_type(self.itemText(i)) for i in range(self.count()) ] config[self.opt_name] = history def do_search(self, *args): self._do_search() def block_signals(self, yes): self.blockSignals(yes) self.line_edit.blockSignals(yes) def set_search_string(self, txt, store_in_history=False, emit_changed=True): if not store_in_history: self.activated[native_string_type].disconnect() try: self.setFocus(Qt.OtherFocusReason) if not txt: self.clear() else: self.normalize_state() # must turn on case sensitivity here so that tag browser strings # are not case-insensitively replaced from history self.line_edit.completer().setCaseSensitivity(Qt.CaseSensitive) self.setEditText(txt) self.line_edit.end(False) if emit_changed: self.changed.emit() self._do_search(store_in_history=store_in_history) self.line_edit.completer().setCaseSensitivity( Qt.CaseInsensitive) self.focus_to_library.emit() finally: if not store_in_history: # QueuedConnection as workaround for https://bugreports.qt-project.org/browse/QTBUG-40807 self.activated[native_string_type].connect( self.history_selected, type=Qt.QueuedConnection) def search_as_you_type(self, enabled): self.as_you_type = enabled def in_a_search(self): return self._in_a_search @property def current_text(self): return unicode_type(self.lineEdit().text())
class Splitter(QSplitter): state_changed = pyqtSignal(object) def __init__(self, name, label, icon, initial_show=True, initial_side_size=120, connect_button=True, orientation=Qt.Horizontal, side_index=0, parent=None, shortcut=None, hide_handle_on_single_panel=True): QSplitter.__init__(self, parent) if hide_handle_on_single_panel: self.state_changed.connect(self.update_handle_width) self.original_handle_width = self.handleWidth() self.resize_timer = QTimer(self) self.resize_timer.setSingleShot(True) self.desired_side_size = initial_side_size self.desired_show = initial_show self.resize_timer.setInterval(5) self.resize_timer.timeout.connect(self.do_resize) self.setOrientation(orientation) self.side_index = side_index self._name = name self.label = label self.initial_side_size = initial_side_size self.initial_show = initial_show self.splitterMoved.connect(self.splitter_moved, type=Qt.QueuedConnection) self.button = LayoutButton(icon, label, self, shortcut=shortcut) if connect_button: self.button.clicked.connect(self.double_clicked) if shortcut is not None: self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label, self) self.action_toggle.changed.connect(self.update_shortcut) self.action_toggle.triggered.connect(self.toggle_triggered) if parent is not None: parent.addAction(self.action_toggle) if hasattr(parent, 'keyboard'): parent.keyboard.register_shortcut('splitter %s %s'%(name, label), unicode_type(self.action_toggle.text()), default_keys=(shortcut,), action=self.action_toggle) else: self.action_toggle.setShortcut(shortcut) else: self.action_toggle.setShortcut(shortcut) def update_shortcut(self): self.button.update_shortcut(self.action_toggle) def toggle_triggered(self, *args): self.toggle_side_pane() def createHandle(self): return SplitterHandle(self.orientation(), self) def initialize(self): for i in range(self.count()): h = self.handle(i) if h is not None: h.splitter_moved() self.state_changed.emit(not self.is_side_index_hidden) def splitter_moved(self, *args): self.desired_side_size = self.side_index_size self.state_changed.emit(not self.is_side_index_hidden) def update_handle_width(self, not_one_panel): self.setHandleWidth(self.original_handle_width if not_one_panel else 0) @property def is_side_index_hidden(self): sizes = list(self.sizes()) try: return sizes[self.side_index] == 0 except IndexError: return True @property def save_name(self): ori = 'horizontal' if self.orientation() == Qt.Horizontal \ else 'vertical' return self._name + '_' + ori def print_sizes(self): if self.count() > 1: print(self.save_name, 'side:', self.side_index_size, 'other:', end=' ') print(list(self.sizes())[self.other_index]) @dynamic_property def side_index_size(self): def fget(self): if self.count() < 2: return 0 return self.sizes()[self.side_index] def fset(self, val): if self.count() < 2: return if val == 0 and not self.is_side_index_hidden: self.save_state() sizes = list(self.sizes()) for i in range(len(sizes)): sizes[i] = val if i == self.side_index else 10 self.setSizes(sizes) total = sum(self.sizes()) sizes = list(self.sizes()) for i in range(len(sizes)): sizes[i] = val if i == self.side_index else total-val self.setSizes(sizes) self.initialize() return property(fget=fget, fset=fset) def do_resize(self, *args): orig = self.desired_side_size QSplitter.resizeEvent(self, self._resize_ev) if orig > 20 and self.desired_show: c = 0 while abs(self.side_index_size - orig) > 10 and c < 5: self.apply_state(self.get_state(), save_desired=False) c += 1 def resizeEvent(self, ev): if self.resize_timer.isActive(): self.resize_timer.stop() self._resize_ev = ev self.resize_timer.start() def get_state(self): if self.count() < 2: return (False, 200) return (self.desired_show, self.desired_side_size) def apply_state(self, state, save_desired=True): if state[0]: self.side_index_size = state[1] if save_desired: self.desired_side_size = self.side_index_size else: self.side_index_size = 0 self.desired_show = state[0] def default_state(self): return (self.initial_show, self.initial_side_size) # Public API {{{ def update_desired_state(self): self.desired_show = not self.is_side_index_hidden def save_state(self): if self.count() > 1: gprefs[self.save_name+'_state'] = self.get_state() @property def other_index(self): return (self.side_index+1)%2 def restore_state(self): if self.count() > 1: state = gprefs.get(self.save_name+'_state', self.default_state()) self.apply_state(state, save_desired=False) self.desired_side_size = state[1] def toggle_side_pane(self, hide=None): if hide is None: action = 'show' if self.is_side_index_hidden else 'hide' else: action = 'hide' if hide else 'show' getattr(self, action+'_side_pane')() def show_side_pane(self): if self.count() < 2 or not self.is_side_index_hidden: return if self.desired_side_size == 0: self.desired_side_size = self.initial_side_size self.apply_state((True, self.desired_side_size)) def hide_side_pane(self): if self.count() < 2 or self.is_side_index_hidden: return self.apply_state((False, self.desired_side_size)) def double_clicked(self, *args): self.toggle_side_pane()
class TagListEditor(QDialog, Ui_TagListEditor): def __init__(self, window, cat_name, tag_to_match, get_book_ids, sorter, ttm_is_first_letter=False): QDialog.__init__(self, window) Ui_TagListEditor.__init__(self) self.setupUi(self) self.verticalLayout_2.setAlignment(Qt.AlignCenter) self.search_box.setMinimumContentsLength(25) # Put the category name into the title bar t = self.windowTitle() self.category_name = cat_name self.setWindowTitle(t + ' (' + cat_name + ')') # Remove help icon on title bar icon = self.windowIcon() self.setWindowFlags(self.windowFlags() & (~Qt.WindowContextHelpButtonHint)) self.setWindowIcon(icon) # Get saved geometry info try: self.table_column_widths = \ gprefs.get('tag_list_editor_table_widths', None) except: pass # initialization self.to_rename = {} self.to_delete = set() self.all_tags = {} self.original_names = {} self.ordered_tags = [] self.sorter = sorter self.get_book_ids = get_book_ids self.text_before_editing = '' # Capture clicks on the horizontal header to sort the table columns hh = self.table.horizontalHeader() hh.sectionResized.connect(self.table_column_resized) hh.setSectionsClickable(True) hh.sectionClicked.connect(self.do_sort) hh.setSortIndicatorShown(True) self.last_sorted_by = 'name' self.name_order = 0 self.count_order = 1 self.was_order = 1 self.edit_delegate = EditColumnDelegate(self.table) self.edit_delegate.editing_finished.connect(self.stop_editing) self.edit_delegate.editing_started.connect(self.start_editing) self.table.setItemDelegateForColumn(0, self.edit_delegate) if prefs['use_primary_find_in_search']: self.string_contains = primary_contains else: self.string_contains = contains self.delete_button.clicked.connect(self.delete_tags) self.table.delete_pressed.connect(self.delete_pressed) self.rename_button.clicked.connect(self.rename_tag) self.undo_button.clicked.connect(self.undo_edit) self.table.itemDoubleClicked.connect(self._rename_tag) self.table.itemChanged.connect(self.finish_editing) self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) self.buttonBox.accepted.connect(self.accepted) self.search_box.initialize('tag_list_search_box_' + cat_name) le = self.search_box.lineEdit() ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) if ac is not None: ac.triggered.connect(self.clear_search) self.search_box.textChanged.connect(self.search_text_changed) self.search_button.clicked.connect(self.do_search) self.search_button.setDefault(True) l = QLabel(self.table) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText(_('No matches found')) l.setAlignment(Qt.AlignVCenter) l.resize(l.sizeHint()) l.move(10, 0) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) self.filter_box.initialize('tag_list_filter_box_' + cat_name) le = self.filter_box.lineEdit() ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) if ac is not None: ac.triggered.connect(self.clear_filter) le.returnPressed.connect(self.do_filter) self.filter_button.clicked.connect(self.do_filter) self.apply_vl_checkbox.clicked.connect(self.vl_box_changed) self.table.setEditTriggers(QTableWidget.EditKeyPressed) try: geom = gprefs.get('tag_list_editor_dialog_geometry', None) if geom is not None: QApplication.instance().safe_restore_geometry( self, QByteArray(geom)) else: self.resize(self.sizeHint() + QSize(150, 100)) except: pass # Add the data self.search_item_row = -1 self.fill_in_table(None, tag_to_match, ttm_is_first_letter) def vl_box_changed(self): self.search_item_row = -1 self.fill_in_table(None, None, False) def do_search(self): self.not_found_label.setVisible(False) find_text = icu_lower(unicode_type(self.search_box.currentText())) if not find_text: return for _ in range(0, self.table.rowCount()): r = self.search_item_row = (self.search_item_row + 1) % self.table.rowCount() if self.string_contains(find_text, self.table.item(r, 0).text()): self.table.setCurrentItem(self.table.item(r, 0)) self.table.setFocus(True) return # Nothing found. Pop up the little dialog for 1.5 seconds self.not_found_label.setVisible(True) self.not_found_label_timer.start(1500) def search_text_changed(self): self.search_item_row = -1 def clear_search(self): self.search_item_row = -1 self.search_box.setText('') def fill_in_table(self, tags, tag_to_match, ttm_is_first_letter): data = self.get_book_ids(self.apply_vl_checkbox.isChecked()) self.all_tags = {} filter_text = icu_lower(unicode_type(self.filter_box.text())) for k, v, count in data: if not filter_text or self.string_contains(filter_text, icu_lower(v)): self.all_tags[v] = { 'key': k, 'count': count, 'cur_name': v, 'is_deleted': k in self.to_delete } self.original_names[k] = v self.edit_delegate.set_completion_data(self.original_names.values()) self.ordered_tags = sorted(self.all_tags.keys(), key=self.sorter) if tags is None: tags = self.ordered_tags select_item = None self.table.blockSignals(True) self.table.clear() self.table.setColumnCount(3) self.name_col = QTableWidgetItem(self.category_name) self.table.setHorizontalHeaderItem(0, self.name_col) self.count_col = QTableWidgetItem(_('Count')) self.table.setHorizontalHeaderItem(1, self.count_col) self.was_col = QTableWidgetItem(_('Was')) self.table.setHorizontalHeaderItem(2, self.was_col) self.table.setRowCount(len(tags)) for row, tag in enumerate(tags): item = NameTableWidgetItem(self.sorter) item.set_is_deleted(self.all_tags[tag]['is_deleted']) _id = self.all_tags[tag]['key'] item.setData(Qt.UserRole, _id) item.set_initial_text(tag) if _id in self.to_rename: item.setText(self.to_rename[_id]) else: item.setText(tag) item.setFlags(item.flags() | Qt.ItemIsSelectable | Qt.ItemIsEditable) self.table.setItem(row, 0, item) if select_item is None: if ttm_is_first_letter: if primary_startswith(tag, tag_to_match): select_item = item elif tag == tag_to_match: select_item = item item = CountTableWidgetItem(self.all_tags[tag]['count']) # only the name column can be selected item.setFlags(item.flags() & ~(Qt.ItemIsSelectable | Qt.ItemIsEditable)) self.table.setItem(row, 1, item) item = QTableWidgetItem() item.setFlags(item.flags() & ~(Qt.ItemIsSelectable | Qt.ItemIsEditable)) if _id in self.to_rename or _id in self.to_delete: item.setData(Qt.DisplayRole, tag) self.table.setItem(row, 2, item) if self.last_sorted_by == 'name': self.table.sortByColumn(0, self.name_order) elif self.last_sorted_by == 'count': self.table.sortByColumn(1, self.count_order) else: self.table.sortByColumn(2, self.was_order) if select_item is not None: self.table.setCurrentItem(select_item) self.table.setFocus(True) self.start_find_pos = select_item.row() else: self.table.setCurrentCell(0, 0) self.search_box.setFocus() self.start_find_pos = -1 self.table.blockSignals(False) def not_found_label_timer_event(self): self.not_found_label.setVisible(False) def clear_filter(self): self.filter_box.setText('') self.fill_in_table(None, None, False) def do_filter(self): self.fill_in_table(None, None, False) def table_column_resized(self, col, old, new): self.table_column_widths = [] for c in range(0, self.table.columnCount()): self.table_column_widths.append(self.table.columnWidth(c)) def resizeEvent(self, *args): QDialog.resizeEvent(self, *args) if self.table_column_widths is not None: for c, w in enumerate(self.table_column_widths): self.table.setColumnWidth(c, w) else: # the vertical scroll bar might not be rendered, so might not yet # have a width. Assume 25. Not a problem because user-changed column # widths will be remembered w = self.table.width() - 25 - self.table.verticalHeader().width() w //= self.table.columnCount() for c in range(0, self.table.columnCount()): self.table.setColumnWidth(c, w) def save_geometry(self): gprefs['tag_list_editor_table_widths'] = self.table_column_widths gprefs['tag_list_editor_dialog_geometry'] = bytearray( self.saveGeometry()) def start_editing(self, on_row): items = self.table.selectedItems() self.table.blockSignals(True) for item in items: if item.row() != on_row: item.set_placeholder(_('Editing...')) else: self.text_before_editing = item.text() self.table.blockSignals(False) def stop_editing(self, on_row): items = self.table.selectedItems() self.table.blockSignals(True) for item in items: if item.row() != on_row and item.is_placeholder: item.reset_placeholder() self.table.blockSignals(False) def finish_editing(self, edited_item): if not edited_item.text(): error_dialog( self, _('Item is blank'), _('An item cannot be set to nothing. Delete it instead.'), show=True) self.table.blockSignals(True) edited_item.setText(self.text_before_editing) self.table.blockSignals(False) return items = self.table.selectedItems() self.table.blockSignals(True) for item in items: id_ = int(item.data(Qt.UserRole)) self.to_rename[id_] = unicode_type(edited_item.text()) orig = self.table.item(item.row(), 2) item.setText(edited_item.text()) orig.setData(Qt.DisplayRole, item.initial_text()) self.table.blockSignals(False) def undo_edit(self): indexes = self.table.selectionModel().selectedRows() if not indexes: error_dialog( self, _('No item selected'), _('You must select one item from the list of Available items.') ).exec_() return if not confirm(_('Do you really want to undo your changes?'), 'tag_list_editor_undo'): return self.table.blockSignals(True) for idx in indexes: row = idx.row() item = self.table.item(row, 0) item.setText(item.initial_text()) item.set_is_deleted(False) self.to_delete.discard(int(item.data(Qt.UserRole))) self.to_rename.pop(int(item.data(Qt.UserRole)), None) self.table.item(row, 2).setData(Qt.DisplayRole, '') self.table.blockSignals(False) def rename_tag(self): item = self.table.item(self.table.currentRow(), 0) self._rename_tag(item) def _rename_tag(self, item): if item is None: error_dialog( self, _('No item selected'), _('You must select one item from the list of Available items.') ).exec_() return col_zero_item = self.table.item(item.row(), 0) if col_zero_item.is_deleted: if not question_dialog( self, _('Undelete item?'), '<p>' + _('That item is deleted. Do you want to undelete it?') + '<br>'): return col_zero_item.set_is_deleted(False) self.to_delete.discard(int(col_zero_item.data(Qt.UserRole))) orig = self.table.item(col_zero_item.row(), 2) self.table.blockSignals(True) orig.setData(Qt.DisplayRole, '') self.table.blockSignals(False) else: self.table.editItem(item) def delete_pressed(self): if self.table.currentColumn() == 0: self.delete_tags() def delete_tags(self): deletes = self.table.selectedItems() if not deletes: error_dialog( self, _('No items selected'), _('You must select at least one item from the list.')).exec_() return to_del = [] for item in deletes: if not item.is_deleted: to_del.append(item) if to_del: ct = ', '.join([unicode_type(item.text()) for item in to_del]) if not confirm( '<p>' + _('Are you sure you want to delete the following items?') + '<br>' + ct, 'tag_list_editor_delete'): return row = self.table.row(deletes[0]) self.table.blockSignals(True) for item in deletes: id_ = int(item.data(Qt.UserRole)) self.to_delete.add(id_) item.set_is_deleted(True) orig = self.table.item(item.row(), 2) orig.setData(Qt.DisplayRole, item.initial_text()) self.table.blockSignals(False) if row >= self.table.rowCount(): row = self.table.rowCount() - 1 if row >= 0: self.table.scrollToItem(self.table.item(row, 0)) def do_sort(self, section): (self.do_sort_by_name, self.do_sort_by_count, self.do_sort_by_was)[section]() def do_sort_by_name(self): self.name_order = 1 - self.name_order self.last_sorted_by = 'name' self.table.sortByColumn(0, self.name_order) def do_sort_by_count(self): self.count_order = 1 - self.count_order self.last_sorted_by = 'count' self.table.sortByColumn(1, self.count_order) def do_sort_by_was(self): self.was_order = 1 - self.was_order self.last_sorted_by = 'count' self.table.sortByColumn(2, self.was_order) def accepted(self): self.save_geometry()
class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): def __init__(self, parent, db, id_to_select, select_sort, select_link): QDialog.__init__(self, parent) Ui_EditAuthorsDialog.__init__(self) self.setupUi(self) # Remove help icon on title bar icon = self.windowIcon() self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) self.setWindowIcon(icon) try: self.table_column_widths = \ gprefs.get('manage_authors_table_widths', None) geom = gprefs.get('manage_authors_dialog_geometry', bytearray('')) self.restoreGeometry(QByteArray(geom)) except: pass self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) self.buttonBox.accepted.connect(self.accepted) # Set up the column headings self.table.setSelectionMode(QAbstractItemView.SingleSelection) self.table.setColumnCount(3) self.down_arrow_icon = QIcon(I('arrow-down.png')) self.up_arrow_icon = QIcon(I('arrow-up.png')) self.blank_icon = QIcon(I('blank.png')) self.auth_col = QTableWidgetItem(_('Author')) self.table.setHorizontalHeaderItem(0, self.auth_col) self.auth_col.setIcon(self.blank_icon) self.aus_col = QTableWidgetItem(_('Author sort')) self.table.setHorizontalHeaderItem(1, self.aus_col) self.aus_col.setIcon(self.up_arrow_icon) self.aul_col = QTableWidgetItem(_('Link')) self.table.setHorizontalHeaderItem(2, self.aul_col) self.aus_col.setIcon(self.blank_icon) # Add the data self.authors = {} auts = db.get_authors_with_ids() self.table.setRowCount(len(auts)) select_item = None for row, (id, author, sort, link) in enumerate(auts): author = author.replace('|', ',') self.authors[id] = (author, sort, link) aut = tableItem(author) aut.setData(Qt.UserRole, id) sort = tableItem(sort) link = tableItem(link) self.table.setItem(row, 0, aut) self.table.setItem(row, 1, sort) self.table.setItem(row, 2, link) if id_to_select in (id, author): if select_sort: select_item = sort elif select_link: select_item = link else: select_item = aut self.table.resizeColumnsToContents() if self.table.columnWidth(2) < 200: self.table.setColumnWidth(2, 200) # set up the cellChanged signal only after the table is filled self.table.cellChanged.connect(self.cell_changed) # set up sort buttons self.sort_by_author.setCheckable(True) self.sort_by_author.setChecked(False) self.sort_by_author.clicked.connect(self.do_sort_by_author) self.author_order = 1 self.table.sortByColumn(1, Qt.AscendingOrder) self.sort_by_author_sort.clicked.connect(self.do_sort_by_author_sort) self.sort_by_author_sort.setCheckable(True) self.sort_by_author_sort.setChecked(True) self.author_sort_order = 1 self.recalc_author_sort.clicked.connect(self.do_recalc_author_sort) self.auth_sort_to_author.clicked.connect(self.do_auth_sort_to_author) # Position on the desired item if select_item is not None: self.table.setCurrentItem(select_item) self.table.editItem(select_item) self.start_find_pos = select_item.row() * 2 + select_item.column() else: self.table.setCurrentCell(0, 0) self.start_find_pos = -1 # set up the search box self.find_box.initialize('manage_authors_search') self.find_box.lineEdit().returnPressed.connect(self.do_find) self.find_box.editTextChanged.connect(self.find_text_changed) self.find_button.clicked.connect(self.do_find) self.find_button.setDefault(True) l = QLabel(self.table) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText(_('No matches found')) l.setAlignment(Qt.AlignVCenter) l.resize(l.sizeHint()) l.move(10,20) l.setVisible(False) self.not_found_label.move(40, 40) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) self.table.setContextMenuPolicy(Qt.CustomContextMenu) self.table.customContextMenuRequested .connect(self.show_context_menu) def save_state(self): self.table_column_widths = [] for c in range(0, self.table.columnCount()): self.table_column_widths.append(self.table.columnWidth(c)) gprefs['manage_authors_table_widths'] = self.table_column_widths gprefs['manage_authors_dialog_geometry'] = bytearray(self.saveGeometry()) def resizeEvent(self, *args): QDialog.resizeEvent(self, *args) if self.table_column_widths is not None: for c,w in enumerate(self.table_column_widths): self.table.setColumnWidth(c, w) else: # the vertical scroll bar might not be rendered, so might not yet # have a width. Assume 25. Not a problem because user-changed column # widths will be remembered w = self.table.width() - 25 - self.table.verticalHeader().width() w /= self.table.columnCount() for c in range(0, self.table.columnCount()): self.table.setColumnWidth(c, w) self.save_state() def show_context_menu(self, point): self.context_item = self.table.itemAt(point) case_menu = QMenu(_('Change Case')) action_upper_case = case_menu.addAction(_('Upper Case')) action_lower_case = case_menu.addAction(_('Lower Case')) action_swap_case = case_menu.addAction(_('Swap Case')) action_title_case = case_menu.addAction(_('Title Case')) action_capitalize = case_menu.addAction(_('Capitalize')) action_upper_case.triggered.connect(self.upper_case) action_lower_case.triggered.connect(self.lower_case) action_swap_case.triggered.connect(self.swap_case) action_title_case.triggered.connect(self.title_case) action_capitalize.triggered.connect(self.capitalize) m = self.au_context_menu = QMenu() ca = m.addAction(_('Copy')) ca.triggered.connect(self.copy_to_clipboard) ca = m.addAction(_('Paste')) ca.triggered.connect(self.paste_from_clipboard) m.addSeparator() if self.context_item is not None and self.context_item.column() == 0: ca = m.addAction(_('Copy to author sort')) ca.triggered.connect(self.copy_au_to_aus) else: ca = m.addAction(_('Copy to author')) ca.triggered.connect(self.copy_aus_to_au) m.addSeparator() m.addMenu(case_menu) m.exec_(self.table.mapToGlobal(point)) def copy_to_clipboard(self): cb = QApplication.clipboard() cb.setText(unicode(self.context_item.text())) def paste_from_clipboard(self): cb = QApplication.clipboard() self.context_item.setText(cb.text()) def upper_case(self): self.context_item.setText(icu_upper(unicode(self.context_item.text()))) def lower_case(self): self.context_item.setText(icu_lower(unicode(self.context_item.text()))) def swap_case(self): self.context_item.setText(unicode(self.context_item.text()).swapcase()) def title_case(self): from calibre.utils.titlecase import titlecase self.context_item.setText(titlecase(unicode(self.context_item.text()))) def capitalize(self): from calibre.utils.icu import capitalize self.context_item.setText(capitalize(unicode(self.context_item.text()))) def copy_aus_to_au(self): row = self.context_item.row() dest = self.table.item(row, 0) dest.setText(self.context_item.text()) def copy_au_to_aus(self): row = self.context_item.row() dest = self.table.item(row, 1) dest.setText(self.context_item.text()) def not_found_label_timer_event(self): self.not_found_label.setVisible(False) def find_text_changed(self): self.start_find_pos = -1 def do_find(self): self.not_found_label.setVisible(False) # For some reason the button box keeps stealing the RETURN shortcut. # Steal it back self.buttonBox.button(QDialogButtonBox.Ok).setDefault(False) self.buttonBox.button(QDialogButtonBox.Ok).setAutoDefault(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(False) self.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False) st = icu_lower(unicode(self.find_box.currentText())) for i in range(0, self.table.rowCount()*2): self.start_find_pos = (self.start_find_pos + 1) % (self.table.rowCount()*2) r = (self.start_find_pos/2)%self.table.rowCount() c = self.start_find_pos % 2 item = self.table.item(r, c) text = icu_lower(unicode(item.text())) if st in text: self.table.setCurrentItem(item) self.table.setFocus(True) return # Nothing found. Pop up the little dialog for 1.5 seconds self.not_found_label.setVisible(True) self.not_found_label_timer.start(1500) def do_sort_by_author(self): self.author_order = 1 if self.author_order == 0 else 0 self.table.sortByColumn(0, self.author_order) self.sort_by_author.setChecked(True) self.sort_by_author_sort.setChecked(False) self.auth_col.setIcon(self.down_arrow_icon if self.author_order else self.up_arrow_icon) self.aus_col.setIcon(self.blank_icon) def do_sort_by_author_sort(self): self.author_sort_order = 1 if self.author_sort_order == 0 else 0 self.table.sortByColumn(1, self.author_sort_order) self.sort_by_author.setChecked(False) self.sort_by_author_sort.setChecked(True) self.aus_col.setIcon(self.down_arrow_icon if self.author_sort_order else self.up_arrow_icon) self.auth_col.setIcon(self.blank_icon) def accepted(self): self.save_state() self.result = [] for row in range(0,self.table.rowCount()): id = int(self.table.item(row, 0).data(Qt.UserRole)) aut = unicode(self.table.item(row, 0).text()).strip() sort = unicode(self.table.item(row, 1).text()).strip() link = unicode(self.table.item(row, 2).text()).strip() orig_aut,orig_sort,orig_link = self.authors[id] if orig_aut != aut or orig_sort != sort or orig_link != link: self.result.append((id, orig_aut, aut, sort, link)) def do_recalc_author_sort(self): self.table.cellChanged.disconnect() for row in range(0,self.table.rowCount()): item = self.table.item(row, 0) aut = unicode(item.text()).strip() c = self.table.item(row, 1) # Sometimes trailing commas are left by changing between copy algs c.setText(author_to_author_sort(aut).rstrip(',')) self.table.setFocus(Qt.OtherFocusReason) self.table.cellChanged.connect(self.cell_changed) def do_auth_sort_to_author(self): self.table.cellChanged.disconnect() for row in range(0,self.table.rowCount()): item = self.table.item(row, 1) aus = unicode(item.text()).strip() c = self.table.item(row, 0) # Sometimes trailing commas are left by changing between copy algs c.setText(aus) self.table.setFocus(Qt.OtherFocusReason) self.table.cellChanged.connect(self.cell_changed) def cell_changed(self, row, col): if col == 0: item = self.table.item(row, 0) aut = unicode(item.text()).strip() amper = aut.find('&') if amper >= 0: error_dialog(self.parent(), _('Invalid author name'), _('Author names cannot contain & characters.')).exec_() aut = aut.replace('&', '%') self.table.item(row, 0).setText(aut) c = self.table.item(row, 1) c.setText(author_to_author_sort(aut)) item = c else: item = self.table.item(row, col) self.table.setCurrentItem(item) self.table.scrollToItem(item)
class LiveCSS(QWidget): goto_declaration = pyqtSignal(object) def __init__(self, preview, parent=None): QWidget.__init__(self, parent) self.preview = preview self.preview_is_refreshing = False self.refresh_needed = False preview.refresh_starting.connect(self.preview_refresh_starting) preview.refreshed.connect(self.preview_refreshed) self.apply_theme() self.setAutoFillBackground(True) self.update_timer = QTimer(self) self.update_timer.timeout.connect(self.update_data) self.update_timer.setSingleShot(True) self.update_timer.setInterval(500) self.now_showing = (None, None, None) self.stack = s = QStackedLayout(self) self.setLayout(s) self.clear_label = la = QLabel( '<h3>' + _('No style information found') + '</h3><p>' + _('Move the cursor inside a HTML tag to see what styles' ' apply to that tag.')) la.setWordWrap(True) la.setAlignment(Qt.AlignTop | Qt.AlignLeft) s.addWidget(la) self.box = box = Box(self) box.hyperlink_activated.connect(self.goto_declaration, type=Qt.QueuedConnection) self.scroll = sc = QScrollArea(self) sc.setWidget(box) sc.setWidgetResizable(True) s.addWidget(sc) def preview_refresh_starting(self): self.preview_is_refreshing = True def preview_refreshed(self): self.preview_is_refreshing = False # We must let the event loop run otherwise the webview will return # stale data in read_data() self.refresh_needed = True self.start_update_timer() def apply_theme(self): f = self.font() f.setFamily(tprefs['editor_font_family'] or default_font_family()) f.setPointSize(tprefs['editor_font_size']) self.setFont(f) theme = get_theme(tprefs['editor_theme']) pal = self.palette() pal.setColor(pal.Window, theme_color(theme, 'Normal', 'bg')) pal.setColor(pal.WindowText, theme_color(theme, 'Normal', 'fg')) pal.setColor(pal.AlternateBase, theme_color(theme, 'HighlightRegion', 'bg')) pal.setColor(pal.Link, theme_color(theme, 'Link', 'fg')) pal.setColor(pal.LinkVisited, theme_color(theme, 'Keyword', 'fg')) self.setPalette(pal) if hasattr(self, 'box'): self.box.relayout() self.update() def clear(self): self.stack.setCurrentIndex(0) def show_data(self, editor_name, sourceline, tags): if self.preview_is_refreshing: return if sourceline is None: self.clear() else: data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: if editor_name == self.current_name and ( editor_name, sourceline, tags) == self.now_showing: # Try again in a little while in case there was a transient # error in the web view self.start_update_timer() return if self.now_showing == ( None, None, None) or self.now_showing[0] != self.current_name: self.clear() return # Try to refresh the data for the currently shown tag instead # of clearing editor_name, sourceline, tags = self.now_showing data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: self.clear() return self.now_showing = (editor_name, sourceline, tags) data['html_name'] = editor_name self.box.show_data(data) self.refresh_needed = False self.stack.setCurrentIndex(1) def read_data(self, sourceline, tags): mf = self.preview.view.page().mainFrame() tags = [x.lower() for x in tags] result = unicode_type( mf.evaluateJavaScript( 'window.calibre_preview_integration.live_css(%s, %s)' % (json.dumps(sourceline), json.dumps(tags))) or '') try: result = json.loads(result) except ValueError: result = None if result is not None: maximum_specificities = {} for node in result['nodes']: is_ancestor = node['is_ancestor'] for rule in node['css']: self.process_rule(rule, is_ancestor, maximum_specificities) for node in result['nodes']: for rule in node['css']: for prop in rule['properties']: if prop.specificity < maximum_specificities[prop.name]: prop.is_overriden = True return result def process_rule(self, rule, is_ancestor, maximum_specificities): selector = rule['selector'] sheet_index = rule['sheet_index'] rule_address = rule['rule_address'] or () if selector is not None: try: specificity = [0] + list(parse(selector)[0].specificity()) except (AttributeError, TypeError, SelectorError): specificity = [0, 0, 0, 0] else: # style attribute specificity = [1, 0, 0, 0] specificity.extend((sheet_index, tuple(rule_address))) ancestor_specificity = 0 if is_ancestor else 1 properties = [] for prop in rule['properties']: important = 1 if prop[-1] == 'important' else 0 p = Property(prop, [ancestor_specificity] + [important] + specificity) properties.append(p) if p.specificity > maximum_specificities.get( p.name, (0, 0, 0, 0, 0, 0)): maximum_specificities[p.name] = p.specificity rule['properties'] = properties href = rule['href'] if hasattr(href, 'startswith') and href.startswith( '%s://%s' % (FAKE_PROTOCOL, FAKE_HOST)): qurl = QUrl(href) name = qurl.path()[1:] if name: rule['href'] = name @property def current_name(self): return self.preview.current_name @property def is_visible(self): return self.isVisible() def showEvent(self, ev): self.update_timer.start() actions['auto-reload-preview'].setEnabled(True) return QWidget.showEvent(self, ev) def sync_to_editor(self): self.update_data() def update_data(self): if not self.is_visible or self.preview_is_refreshing: return editor_name = self.current_name ed = editors.get(editor_name, None) if self.update_timer.isActive() or (ed is None and editor_name is not None): return QTimer.singleShot(100, self.update_data) if ed is not None: sourceline, tags = ed.current_tag(for_position_sync=False) if self.refresh_needed or self.now_showing != (editor_name, sourceline, tags): self.show_data(editor_name, sourceline, tags) def start_update_timer(self): if self.is_visible: self.update_timer.start() def stop_update_timer(self): self.update_timer.stop() def navigate_to_declaration(self, data, editor): if data['type'] == 'inline': sourceline, tags = data['sourceline_address'] editor.goto_sourceline(sourceline, tags, attribute='style') elif data['type'] == 'sheet': editor.goto_css_rule(data['rule_address']) elif data['type'] == 'elem': editor.goto_css_rule(data['rule_address'], sourceline_address=data['sourceline_address'])