def genesis(self): md = self.qaction.menu() cm = partial(self.create_menu_action, md) cm('individual', _('Edit metadata individually'), icon=self.qaction.icon(), triggered=partial(self.edit_metadata, False, bulk=False)) md.addSeparator() cm('bulk', _('Edit metadata in bulk'), triggered=partial(self.edit_metadata, False, bulk=True)) md.addSeparator() cm('download', _('Download metadata and covers'), triggered=partial(self.download_metadata, ids=None), shortcut='Ctrl+D') self.metadata_menu = md mb = QMenu() cm2 = partial(self.create_menu_action, mb) cm2('merge delete', _('Merge into first selected book - delete others'), triggered=self.merge_books) mb.addSeparator() cm2('merge keep', _('Merge into first selected book - keep others'), triggered=partial(self.merge_books, safe_merge=True), shortcut='Alt+M') mb.addSeparator() cm2('merge formats', _('Merge only formats into first selected book - delete others'), triggered=partial(self.merge_books, merge_only_formats=True), shortcut='Alt+Shift+M') self.merge_menu = mb md.addSeparator() self.action_merge = cm('merge', _('Merge book records'), icon='merge_books.png', shortcut=_('M'), triggered=self.merge_books) self.action_merge.setMenu(mb) self.qaction.triggered.connect(self.edit_metadata)
def show_context_menu(self, pos): m = QMenu(self) a = m.addAction for x in ('undo', 'redo'): a(actions['editor-%s' % x]) m.addSeparator() for x in ('cut', 'copy', 'paste'): a(actions['editor-' + x]) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) m.addAction(actions['mark-selected-text']) m.exec_(self.editor.mapToGlobal(pos))
def show_context_menu(self, pos): m = QMenu(self) a = m.addAction for x in ('undo', 'redo'): a(actions['editor-%s' % x]) m.addSeparator() for x in ('cut', 'copy', 'paste'): a(actions['editor-' + x]) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) m.addAction(actions['mark-selected-text']) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.mapToGlobal(pos))
def contextMenuEvent(self, event): index = self.indexAt(event.pos()) if not index.isValid(): return result = self.model().get_result(index) menu = QMenu() da = menu.addAction(_('Download...'), partial(self.download_requested.emit, result)) if not result.downloads: da.setEnabled(False) menu.addSeparator() menu.addAction(_('Goto in store...'), partial(self.open_requested.emit, result)) menu.exec_(event.globalPos())
def genesis(self): md = self.qaction.menu() cm = partial(self.create_menu_action, md) cm( "individual", _("Edit metadata individually"), icon=self.qaction.icon(), triggered=partial(self.edit_metadata, False, bulk=False), ) md.addSeparator() cm("bulk", _("Edit metadata in bulk"), triggered=partial(self.edit_metadata, False, bulk=True)) md.addSeparator() cm( "download", _("Download metadata and covers"), triggered=partial(self.download_metadata, ids=None), shortcut="Ctrl+D", ) self.metadata_menu = md mb = QMenu() cm2 = partial(self.create_menu_action, mb) cm2("merge delete", _("Merge into first selected book - delete others"), triggered=self.merge_books) mb.addSeparator() cm2( "merge keep", _("Merge into first selected book - keep others"), triggered=partial(self.merge_books, safe_merge=True), shortcut="Alt+M", ) mb.addSeparator() cm2( "merge formats", _("Merge only formats into first selected book - delete others"), triggered=partial(self.merge_books, merge_only_formats=True), shortcut="Alt+Shift+M", ) self.merge_menu = mb md.addSeparator() self.action_merge = cm( "merge", _("Merge book records"), icon="merge_books.png", shortcut=_("M"), triggered=self.merge_books ) self.action_merge.setMenu(mb) self.qaction.triggered.connect(self.edit_metadata)
def eventFilter(self, obj, event): base = super(Central, self) if obj is not self.editor_tabs.tabBar() or event.type() != QEvent.MouseButtonPress or event.button() not in (Qt.RightButton, Qt.MidButton): return base.eventFilter(obj, event) index = self.editor_tabs.tabBar().tabAt(event.pos()) if index < 0: return base.eventFilter(obj, event) if event.button() == Qt.MidButton: self._close_requested(index) ed = self.editor_tabs.widget(index) if ed is not None: menu = QMenu(self) menu.addAction(actions['close-current-tab'].icon(), _('Close tab'), partial(self.close_requested.emit, ed)) menu.addSeparator() menu.addAction(actions['close-all-but-current-tab'].icon(), _('Close other tabs'), partial(self.close_all_but, ed)) menu.exec_(self.editor_tabs.tabBar().mapToGlobal(event.pos())) return True
def contextMenuEvent(self, event): menu = QMenu(self) pos = event.pos() index = self.indexAt(pos) if index.isValid(): item = self.indexAt(pos).internalPointer() node = self.item_to_node_dict[item] nodeset = [node] # ? ? ? ? optflag = False # ? ? ? ? cmenu_spec = self.ne1model.make_cmenuspec_for_set(nodeset, optflag) for x in cmenu_spec: if x is not None: str, thunk = x[:2] act = QAction(str, self) act.setEnabled("disabled" not in x[2:]) self.connect(act, SIGNAL("triggered()"), thunk) menu.addAction(act) else: menu.addSeparator() menu.exec_(event.globalPos())
def contextMenuEvent(self, ev): m = QMenu(self) w = self.model().word_for_row(self.currentIndex().row()) if w is not None: a = m.addAction(_('Change %s to') % w[0]) cm = QMenu() a.setMenu(cm) cm.addAction(_('Specify replacement manually'), partial(self.change_to.emit, w, None)) cm.addSeparator() for s in dictionaries.suggestions(*w): cm.addAction(s, partial(self.change_to.emit, w, s)) m.addAction(_('Ignore/Unignore all selected words'), self.ignore_all) a = m.addAction(_('Add/Remove all selected words')) am = QMenu() a.setMenu(am) for dic in sorted(dictionaries.active_user_dictionaries, key=lambda x:sort_key(x.name)): am.addAction(dic.name, partial(self.add_all.emit, dic.name)) m.exec_(ev.globalPos())
def contextMenuEvent(self, event): menu = QMenu(self) pos = event.pos() index = self.indexAt(pos) if index.isValid(): item = self.indexAt(pos).internalPointer() node = self.item_to_node_dict[item] nodeset = [ node ] # ? ? ? ? optflag = False # ? ? ? ? cmenu_spec = self.treemodel.make_cmenuspec_for_set(nodeset, optflag) for x in cmenu_spec: if x is not None: str, thunk = x[:2] act = QAction(str, self) act.setEnabled("disabled" not in x[2:]) self.connect(act, SIGNAL("triggered()"), thunk) menu.addAction(act) else: menu.addSeparator() menu.exec_(event.globalPos())
def show_context_menu(self, pos): m = QMenu(self) a = m.addAction c = self.editor.cursorForPosition(pos) r = self.editor.syntax_range_for_cursor(c) if r is not None and r.format.property(SPELL_PROPERTY).toBool(): word = self.editor.text_for_range(c.block(), r) locale = self.editor.spellcheck_locale_for_cursor(c) orig_pos = c.position() c.setPosition(orig_pos - utf16_length(word)) found = False self.editor.setTextCursor(c) if self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False): found = True fc = self.editor.textCursor() if fc.position() < c.position(): self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False) if found: suggestions = dictionaries.suggestions(word, locale)[:7] if suggestions: for suggestion in suggestions: ac = m.addAction(suggestion, partial(self.editor.simple_replace, suggestion)) f = ac.font() f.setBold(True), ac.setFont(f) m.addSeparator() m.addAction(actions['spell-next']) m.addAction(_('Ignore this word'), partial(self._nuke_word, None, word, locale)) dics = dictionaries.active_user_dictionaries if len(dics) > 0: if len(dics) == 1: m.addAction(_('Add this word to the dictionary: {0}').format(dics[0].name), partial( self._nuke_word, dics[0].name, word, locale)) else: ac = m.addAction(_('Add this word to the dictionary')) dmenu = QMenu(m) ac.setMenu(dmenu) for dic in dics: dmenu.addAction(dic.name, partial(self._nuke_word, dic.name, word, locale)) m.addSeparator() for x in ('undo', 'redo'): a(actions['editor-%s' % x]) m.addSeparator() for x in ('cut', 'copy', 'paste'): a(actions['editor-' + x]) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) m.addAction(actions['mark-selected-text']) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.mapToGlobal(pos))
def show_context_menu(self, point): item = self.itemAt(point) if item is None or item in set(self.categories.itervalues()): return m = QMenu(self) sel = self.selectedItems() num = len(sel) container = current_container() ci = self.currentItem() if ci is not None: cn = unicode(ci.data(0, NAME_ROLE).toString()) mt = unicode(ci.data(0, MIME_ROLE).toString()) cat = unicode(ci.data(0, CATEGORY_ROLE).toString()) n = elided_text(cn.rpartition('/')[-1]) m.addAction(QIcon(I('save.png')), _('Export %s') % n, partial(self.export, cn)) if cn not in container.names_that_must_not_be_changed and cn not in container.names_that_must_not_be_removed and mt not in OEB_FONTS: m.addAction(_('Replace %s with file...') % n, partial(self.replace, cn)) m.addSeparator() m.addAction(QIcon(I('modified.png')), _('&Rename %s') % n, self.edit_current_item) if is_raster_image(mt): m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover image') % n, partial(self.mark_as_cover, cn)) elif current_container().SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text': m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover page') % n, partial(self.mark_as_titlepage, cn)) m.addSeparator() if num > 0: m.addSeparator() if num > 1: m.addAction(QIcon(I('modified.png')), _('&Bulk rename selected files'), self.request_bulk_rename) m.addAction(QIcon(I('trash.png')), _('&Delete the %d selected file(s)') % num, self.request_delete) m.addSeparator() selected_map = defaultdict(list) for item in sel: selected_map[unicode(item.data(0, CATEGORY_ROLE).toString())].append(unicode(item.data(0, NAME_ROLE).toString())) for items in selected_map.itervalues(): items.sort(key=self.index_of_name) if selected_map['text']: m.addAction(QIcon(I('format-text-color.png')), _('Link &stylesheets...'), partial(self.link_stylesheets, selected_map['text'])) if len(selected_map['text']) > 1: m.addAction(QIcon(I('merge.png')), _('&Merge selected text files'), partial(self.start_merge, 'text', selected_map['text'])) if len(selected_map['styles']) > 1: m.addAction(QIcon(I('merge.png')), _('&Merge selected style files'), partial(self.start_merge, 'styles', selected_map['styles'])) if len(list(m.actions())) > 0: m.popup(self.mapToGlobal(point))
class BooksView(QTableView): # {{{ files_dropped = pyqtSignal(object) add_column_signal = pyqtSignal() def viewportEvent(self, event): if (event.type() == event.ToolTip and not gprefs['book_list_tooltips']): return False return QTableView.viewportEvent(self, event) def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True): QTableView.__init__(self, parent) self.gui = parent self.setProperty('highlight_current_item', 150) self.row_sizing_done = False self.alternate_views = AlternateViews(self) if not tweaks['horizontal_scrolling_per_column']: self.setHorizontalScrollMode(self.ScrollPerPixel) self.setEditTriggers(self.EditKeyPressed) if tweaks['doubleclick_on_library_view'] == 'edit_cell': self.setEditTriggers(self.DoubleClicked|self.editTriggers()) elif tweaks['doubleclick_on_library_view'] == 'open_viewer': self.setEditTriggers(self.SelectedClicked|self.editTriggers()) self.doubleClicked.connect(parent.iactions['View'].view_triggered) elif tweaks['doubleclick_on_library_view'] == 'edit_metadata': # Must not enable single-click to edit, or the field will remain # open in edit mode underneath the edit metadata dialog if use_edit_metadata_dialog: self.doubleClicked.connect( partial(parent.iactions['Edit Metadata'].edit_metadata, checked=False)) else: self.setEditTriggers(self.DoubleClicked|self.editTriggers()) setup_dnd_interface(self) self.setAlternatingRowColors(True) self.setSelectionBehavior(self.SelectRows) self.setShowGrid(False) self.setWordWrap(False) self.rating_delegate = RatingDelegate(self) self.timestamp_delegate = DateDelegate(self) self.pubdate_delegate = PubDateDelegate(self) self.last_modified_delegate = DateDelegate(self, tweak_name='gui_last_modified_display_format') self.languages_delegate = LanguagesDelegate(self) self.tags_delegate = CompleteDelegate(self, ',', 'all_tag_names') self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True) self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True) self.series_delegate = TextDelegate(self) self.publisher_delegate = TextDelegate(self) self.text_delegate = TextDelegate(self) self.cc_text_delegate = CcTextDelegate(self) self.cc_enum_delegate = CcEnumDelegate(self) self.cc_bool_delegate = CcBoolDelegate(self) self.cc_comments_delegate = CcCommentsDelegate(self) self.cc_template_delegate = CcTemplateDelegate(self) self.cc_number_delegate = CcNumberDelegate(self) self.display_parent = parent self._model = modelcls(self) self.setModel(self._model) self._model.count_changed_signal.connect(self.do_row_sizing, type=Qt.QueuedConnection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSortingEnabled(True) self.selectionModel().currentRowChanged.connect(self._model.current_changed) self.preserve_state = partial(PreserveViewState, self) # {{{ Column Header setup self.can_add_columns = True self.was_restored = False self.column_header = HeaderView(Qt.Horizontal, self) self.setHorizontalHeader(self.column_header) self.column_header.setMovable(True) self.column_header.setClickable(True) self.column_header.sectionMoved.connect(self.save_state) self.column_header.setContextMenuPolicy(Qt.CustomContextMenu) self.column_header.customContextMenuRequested.connect(self.show_column_header_context_menu) self.column_header.sectionResized.connect(self.column_resized, Qt.QueuedConnection) self.row_header = HeaderView(Qt.Vertical, self) self.setVerticalHeader(self.row_header) # }}} self._model.database_changed.connect(self.database_changed) hv = self.verticalHeader() hv.setClickable(True) hv.setCursor(Qt.PointingHandCursor) self.selected_ids = [] self._model.about_to_be_sorted.connect(self.about_to_be_sorted) self._model.sorting_done.connect(self.sorting_done, type=Qt.QueuedConnection) # Column Header Context Menu {{{ def column_header_context_handler(self, action=None, column=None): if not action or not column: return try: idx = self.column_map.index(column) except: return h = self.column_header if action == 'hide': h.setSectionHidden(idx, True) elif action == 'show': h.setSectionHidden(idx, False) if h.sectionSize(idx) < 3: sz = h.sectionSizeHint(idx) h.resizeSection(idx, sz) elif action == 'ascending': self.sortByColumn(idx, Qt.AscendingOrder) elif action == 'descending': self.sortByColumn(idx, Qt.DescendingOrder) elif action == 'defaults': self.apply_state(self.get_default_state()) elif action == 'addcustcol': self.add_column_signal.emit() elif action.startswith('align_'): alignment = action.partition('_')[-1] self._model.change_alignment(column, alignment) elif action == 'quickview': from calibre.customize.ui import find_plugin qv = find_plugin('Show Quickview') if qv: rows = self.selectionModel().selectedRows() if len(rows) > 0: current_row = rows[0].row() current_col = self.column_map.index(column) index = self.model().index(current_row, current_col) qv.actual_plugin_.change_quickview_column(index) self.save_state() def show_column_header_context_menu(self, pos): idx = self.column_header.logicalIndexAt(pos) if idx > -1 and idx < len(self.column_map): col = self.column_map[idx] name = unicode(self.model().headerData(idx, Qt.Horizontal, Qt.DisplayRole).toString()) self.column_header_context_menu = QMenu(self) if col != 'ondevice': self.column_header_context_menu.addAction(_('Hide column %s') % name, partial(self.column_header_context_handler, action='hide', column=col)) m = self.column_header_context_menu.addMenu( _('Sort on %s') % name) a = m.addAction(_('Ascending'), partial(self.column_header_context_handler, action='ascending', column=col)) d = m.addAction(_('Descending'), partial(self.column_header_context_handler, action='descending', column=col)) if self._model.sorted_on[0] == col: ac = a if self._model.sorted_on[1] else d ac.setCheckable(True) ac.setChecked(True) if col not in ('ondevice', 'inlibrary') and \ (not self.model().is_custom_column(col) or self.model().custom_columns[col]['datatype'] not in ('bool', )): m = self.column_header_context_menu.addMenu( _('Change text alignment for %s') % name) al = self._model.alignment_map.get(col, 'left') for x, t in (('left', _('Left')), ('right', _('Right')), ('center', _('Center'))): a = m.addAction(t, partial(self.column_header_context_handler, action='align_'+x, column=col)) if al == x: a.setCheckable(True) a.setChecked(True) if not isinstance(self, DeviceBooksView): if self._model.db.field_metadata[col]['is_category']: act = self.column_header_context_menu.addAction(_('Quickview column %s') % name, partial(self.column_header_context_handler, action='quickview', column=col)) rows = self.selectionModel().selectedRows() if len(rows) > 1: act.setEnabled(False) hidden_cols = [self.column_map[i] for i in range(self.column_header.count()) if self.column_header.isSectionHidden(i)] try: hidden_cols.remove('ondevice') except: pass if hidden_cols: self.column_header_context_menu.addSeparator() m = self.column_header_context_menu.addMenu(_('Show column')) for col in hidden_cols: hidx = self.column_map.index(col) name = unicode(self.model().headerData(hidx, Qt.Horizontal, Qt.DisplayRole).toString()) m.addAction(name, partial(self.column_header_context_handler, action='show', column=col)) self.column_header_context_menu.addSeparator() self.column_header_context_menu.addAction( _('Shrink column if it is too wide to fit'), partial(self.resize_column_to_fit, column=self.column_map[idx])) self.column_header_context_menu.addAction( _('Restore default layout'), partial(self.column_header_context_handler, action='defaults', column=col)) if self.can_add_columns: self.column_header_context_menu.addAction( QIcon(I('column.png')), _('Add your own columns'), partial(self.column_header_context_handler, action='addcustcol', column=col)) self.column_header_context_menu.popup(self.column_header.mapToGlobal(pos)) # }}} # Sorting {{{ def about_to_be_sorted(self, idc): selected_rows = [r.row() for r in self.selectionModel().selectedRows()] self.selected_ids = [idc(r) for r in selected_rows] def sorting_done(self, indexc): pos = self.horizontalScrollBar().value() self.select_rows(self.selected_ids, using_ids=True, change_current=True, scroll=True) self.selected_ids = [] self.horizontalScrollBar().setValue(pos) def sort_by_named_field(self, field, order, reset=True): if field in self.column_map: idx = self.column_map.index(field) if order: self.sortByColumn(idx, Qt.AscendingOrder) else: self.sortByColumn(idx, Qt.DescendingOrder) else: self._model.sort_by_named_field(field, order, reset) def multisort(self, fields, reset=True, only_if_different=False): if len(fields) == 0: return sh = self.cleanup_sort_history(self._model.sort_history, ignore_column_map=True) if only_if_different and len(sh) >= len(fields): ret=True for i,t in enumerate(fields): if t[0] != sh[i][0]: ret = False break if ret: return for n,d in reversed(fields): if n in self._model.db.field_metadata.keys(): sh.insert(0, (n, d)) sh = self.cleanup_sort_history(sh, ignore_column_map=True) self._model.sort_history = [tuple(x) for x in sh] self._model.resort(reset=reset) col = fields[0][0] dir = Qt.AscendingOrder if fields[0][1] else Qt.DescendingOrder if col in self.column_map: col = self.column_map.index(col) hdrs = self.horizontalHeader() try: hdrs.setSortIndicator(col, dir) except: pass # }}} # Ondevice column {{{ def set_ondevice_column_visibility(self): m = self._model self.column_header.setSectionHidden(m.column_map.index('ondevice'), not m.device_connected) def set_device_connected(self, is_connected): self._model.set_device_connected(is_connected) self.set_ondevice_column_visibility() # }}} # Save/Restore State {{{ def get_state(self): h = self.column_header cm = self.column_map state = {} state['hidden_columns'] = [cm[i] for i in range(h.count()) if h.isSectionHidden(i) and cm[i] != 'ondevice'] state['last_modified_injected'] = True state['languages_injected'] = True state['sort_history'] = \ self.cleanup_sort_history(self.model().sort_history) state['column_positions'] = {} state['column_sizes'] = {} state['column_alignment'] = self._model.alignment_map for i in range(h.count()): name = cm[i] state['column_positions'][name] = h.visualIndex(i) if name != 'ondevice': state['column_sizes'][name] = h.sectionSize(i) return state def write_state(self, state): db = getattr(self.model(), 'db', None) name = unicode(self.objectName()) if name and db is not None: db.prefs.set(name + ' books view state', state) def save_state(self): # Only save if we have been initialized (set_database called) if len(self.column_map) > 0 and self.was_restored: state = self.get_state() self.write_state(state) def cleanup_sort_history(self, sort_history, ignore_column_map=False): history = [] for col, order in sort_history: if not isinstance(order, bool): continue if col == 'date': col = 'timestamp' if ignore_column_map or col in self.column_map: if (not history or history[-1][0] != col): history.append([col, order]) return history def apply_sort_history(self, saved_history, max_sort_levels=3): if not saved_history: return for col, order in reversed(self.cleanup_sort_history( saved_history)[:max_sort_levels]): self.sortByColumn(self.column_map.index(col), Qt.AscendingOrder if order else Qt.DescendingOrder) def apply_state(self, state, max_sort_levels=3): h = self.column_header cmap = {} hidden = state.get('hidden_columns', []) for i, c in enumerate(self.column_map): cmap[c] = i if c != 'ondevice': h.setSectionHidden(i, c in hidden) positions = state.get('column_positions', {}) pmap = {} for col, pos in positions.items(): if col in cmap: pmap[pos] = col for pos in sorted(pmap.keys()): col = pmap[pos] idx = cmap[col] current_pos = h.visualIndex(idx) if current_pos != pos: h.moveSection(current_pos, pos) sizes = state.get('column_sizes', {}) for col, size in sizes.items(): if col in cmap: sz = sizes[col] if sz < 3: sz = h.sectionSizeHint(cmap[col]) h.resizeSection(cmap[col], sz) self.apply_sort_history(state.get('sort_history', None), max_sort_levels=max_sort_levels) for col, alignment in state.get('column_alignment', {}).items(): self._model.change_alignment(col, alignment) for i in range(h.count()): if not h.isSectionHidden(i) and h.sectionSize(i) < 3: sz = h.sectionSizeHint(i) h.resizeSection(i, sz) def get_default_state(self): old_state = { 'hidden_columns': ['last_modified', 'languages'], 'sort_history':[DEFAULT_SORT], 'column_positions': {}, 'column_sizes': {}, 'column_alignment': { 'size':'center', 'timestamp':'center', 'pubdate':'center'}, 'last_modified_injected': True, 'languages_injected': True, } h = self.column_header cm = self.column_map for i in range(h.count()): name = cm[i] old_state['column_positions'][name] = i if name != 'ondevice': old_state['column_sizes'][name] = \ min(350, max(self.sizeHintForColumn(i), h.sectionSizeHint(i))) if name in ('timestamp', 'last_modified'): old_state['column_sizes'][name] += 12 return old_state def get_old_state(self): ans = None name = unicode(self.objectName()) if name: name += ' books view state' db = getattr(self.model(), 'db', None) if db is not None: ans = db.prefs.get(name, None) if ans is None: ans = gprefs.get(name, None) try: del gprefs[name] except: pass if ans is not None: db.prefs[name] = ans else: injected = False if not ans.get('last_modified_injected', False): injected = True ans['last_modified_injected'] = True hc = ans.get('hidden_columns', []) if 'last_modified' not in hc: hc.append('last_modified') if not ans.get('languages_injected', False): injected = True ans['languages_injected'] = True hc = ans.get('hidden_columns', []) if 'languages' not in hc: hc.append('languages') if injected: db.prefs[name] = ans return ans def restore_state(self): old_state = self.get_old_state() if old_state is None: old_state = self.get_default_state() max_levels = 3 if tweaks['sort_columns_at_startup'] is not None: sh = [] try: for c,d in tweaks['sort_columns_at_startup']: if not isinstance(d, bool): d = True if d == 0 else False sh.append((c, d)) except: # Ignore invalid tweak values as users seem to often get them # wrong print('Ignoring invalid sort_columns_at_startup tweak, with error:') import traceback traceback.print_exc() old_state['sort_history'] = sh max_levels = max(3, len(sh)) self.column_header.blockSignals(True) self.apply_state(old_state, max_sort_levels=max_levels) self.column_header.blockSignals(False) self.do_row_sizing() self.was_restored = True def refresh_row_sizing(self): self.row_sizing_done = False self.do_row_sizing() def do_row_sizing(self): # Resize all rows to have the correct height if not self.row_sizing_done and self.model().rowCount(QModelIndex()) > 0: self.resizeRowToContents(0) self.verticalHeader().setDefaultSectionSize(self.rowHeight(0) + gprefs['extra_row_spacing']) self.row_sizing_done = True def resize_column_to_fit(self, column): col = self.column_map.index(column) self.column_resized(col, self.columnWidth(col), self.columnWidth(col)) def column_resized(self, col, old_size, new_size): # arbitrary: scroll bar + header + some max_width = self.width() - (self.verticalScrollBar().width() + self.verticalHeader().width() + 10) if max_width < 200: max_width = 200 if new_size > max_width: self.column_header.blockSignals(True) self.setColumnWidth(col, max_width) self.column_header.blockSignals(False) # }}} # Initialization/Delegate Setup {{{ def set_database(self, db): self.alternate_views.set_database(db) self.save_state() self._model.set_database(db) self.tags_delegate.set_database(db) self.cc_names_delegate.set_database(db) self.authors_delegate.set_database(db) self.series_delegate.set_auto_complete_function(db.all_series) self.publisher_delegate.set_auto_complete_function(db.all_publishers) self.alternate_views.set_database(db, stage=1) def database_changed(self, db): for i in range(self.model().columnCount(None)): if self.itemDelegateForColumn(i) in (self.rating_delegate, self.timestamp_delegate, self.pubdate_delegate, self.last_modified_delegate, self.languages_delegate): self.setItemDelegateForColumn(i, self.itemDelegate()) cm = self.column_map for colhead in cm: if self._model.is_custom_column(colhead): cc = self._model.custom_columns[colhead] if cc['datatype'] == 'datetime': delegate = CcDateDelegate(self) delegate.set_format(cc['display'].get('date_format','')) self.setItemDelegateForColumn(cm.index(colhead), delegate) elif cc['datatype'] == 'comments': self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate) elif cc['datatype'] == 'text': if cc['is_multiple']: if cc['display'].get('is_names', False): self.setItemDelegateForColumn(cm.index(colhead), self.cc_names_delegate) else: self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate) else: self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate) elif cc['datatype'] == 'series': self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate) elif cc['datatype'] in ('int', 'float'): self.setItemDelegateForColumn(cm.index(colhead), self.cc_number_delegate) elif cc['datatype'] == 'bool': self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate) elif cc['datatype'] == 'rating': self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate) elif cc['datatype'] == 'composite': self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate) elif cc['datatype'] == 'enumeration': self.setItemDelegateForColumn(cm.index(colhead), self.cc_enum_delegate) else: dattr = colhead+'_delegate' delegate = colhead if hasattr(self, dattr) else 'text' self.setItemDelegateForColumn(cm.index(colhead), getattr(self, delegate+'_delegate')) self.restore_state() self.set_ondevice_column_visibility() #}}} # Context Menu {{{ def set_context_menu(self, menu, edit_collections_action): self.setContextMenuPolicy(Qt.DefaultContextMenu) self.context_menu = menu self.alternate_views.set_context_menu(menu) self.edit_collections_action = edit_collections_action def contextMenuEvent(self, event): sac = self.gui.iactions['Sort By'] sort_added = tuple(ac for ac in self.context_menu.actions() if ac is sac.qaction) if sort_added: sac.update_menu() self.context_menu.popup(event.globalPos()) event.accept() # }}} @property def column_map(self): return self._model.column_map @property def visible_columns(self): h = self.horizontalHeader() logical_indices = (x for x in xrange(h.count()) if not h.isSectionHidden(x)) rmap = {i:x for i, x in enumerate(self.column_map)} return (rmap[h.visualIndex(x)] for x in logical_indices if h.visualIndex(x) > -1) def refresh_book_details(self): idx = self.currentIndex() if idx.isValid(): self._model.current_changed(idx, idx) def scrollContentsBy(self, dx, dy): # Needed as Qt bug causes headerview to not always update when scrolling QTableView.scrollContentsBy(self, dx, dy) if dy != 0: self.column_header.update() def scroll_to_row(self, row): if row > -1: h = self.horizontalHeader() for i in range(h.count()): if not h.isSectionHidden(i) and h.sectionViewportPosition(i) >= 0: self.scrollTo(self.model().index(row, i), self.PositionAtCenter) break def set_current_row(self, row=0, select=True, for_sync=False): if row > -1 and row < self.model().rowCount(QModelIndex()): h = self.horizontalHeader() logical_indices = list(range(h.count())) logical_indices = [x for x in logical_indices if not h.isSectionHidden(x)] pairs = [(x, h.visualIndex(x)) for x in logical_indices if h.visualIndex(x) > -1] if not pairs: pairs = [(0, 0)] pairs.sort(cmp=lambda x,y:cmp(x[1], y[1])) i = pairs[0][0] index = self.model().index(row, i) if for_sync: sm = self.selectionModel() sm.setCurrentIndex(index, sm.NoUpdate) else: self.setCurrentIndex(index) if select: sm = self.selectionModel() sm.select(index, sm.ClearAndSelect|sm.Rows) def keyPressEvent(self, ev): val = self.horizontalScrollBar().value() ret = super(BooksView, self).keyPressEvent(ev) if ev.isAccepted() and ev.key() in (Qt.Key_Home, Qt.Key_End ) and ev.modifiers() & Qt.ControlModifier: self.horizontalScrollBar().setValue(val) return ret def ids_to_rows(self, ids): row_map = OrderedDict() ids = frozenset(ids) m = self.model() for row in xrange(m.rowCount(QModelIndex())): if len(row_map) >= len(ids): break c = m.id(row) if c in ids: row_map[c] = row return row_map def select_rows(self, identifiers, using_ids=True, change_current=True, scroll=True): ''' Select rows identified by identifiers. identifiers can be a set of ids, row numbers or QModelIndexes. ''' rows = set([x.row() if hasattr(x, 'row') else x for x in identifiers]) if using_ids: rows = set([]) identifiers = set(identifiers) m = self.model() for row in xrange(m.rowCount(QModelIndex())): if m.id(row) in identifiers: rows.add(row) rows = list(sorted(rows)) if rows: row = rows[0] if change_current: self.set_current_row(row, select=False) if scroll: self.scroll_to_row(row) sm = self.selectionModel() sel = QItemSelection() m = self.model() max_col = m.columnCount(QModelIndex()) - 1 # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda (i,x):i-x): group = list(map(operator.itemgetter(1), g)) sel.merge(QItemSelection(m.index(min(group), 0), m.index(max(group), max_col)), sm.Select) sm.select(sel, sm.ClearAndSelect)
class LocationManager(QObject): # {{{ locations_changed = pyqtSignal() unmount_device = pyqtSignal() location_selected = pyqtSignal(object) configure_device = pyqtSignal() def __init__(self, parent=None): QObject.__init__(self, parent) self.free = [-1, -1, -1] self.count = 0 self.location_actions = QActionGroup(self) self.location_actions.setExclusive(True) self.current_location = 'library' self._mem = [] self.tooltips = {} self.all_actions = [] def ac(name, text, icon, tooltip): icon = QIcon(I(icon)) ac = self.location_actions.addAction(icon, text) setattr(self, 'location_' + name, ac) ac.setAutoRepeat(False) ac.setCheckable(True) receiver = partial(self._location_selected, name) ac.triggered.connect(receiver) self.tooltips[name] = tooltip m = QMenu(parent) self._mem.append(m) a = m.addAction(icon, tooltip) a.triggered.connect(receiver) if name != 'library': self._mem.append(a) a = m.addAction(QIcon(I('eject.png')), _('Eject this device')) a.triggered.connect(self._eject_requested) self._mem.append(a) a = m.addAction(QIcon(I('config.png')), _('Configure this device')) a.triggered.connect(self._configure_requested) self._mem.append(a) else: ac.setToolTip(tooltip) ac.setMenu(m) ac.calibre_name = name self.all_actions.append(ac) return ac self.library_action = ac('library', _('Library'), 'lt.png', _('Show books in calibre library')) ac('main', _('Device'), 'reader.png', _('Show books in the main memory of the device')) ac('carda', _('Card A'), 'sd.png', _('Show books in storage card A')) ac('cardb', _('Card B'), 'sd.png', _('Show books in storage card B')) def set_switch_actions(self, quick_actions, rename_actions, delete_actions, switch_actions, choose_action): self.switch_menu = self.library_action.menu() if self.switch_menu: self.switch_menu.addSeparator() else: self.switch_menu = QMenu() self.switch_menu.addAction(choose_action) self.cs_menus = [] for t, acs in [(_('Quick switch'), quick_actions), (_('Rename library'), rename_actions), (_('Delete library'), delete_actions)]: if acs: self.cs_menus.append(QMenu(t)) for ac in acs: self.cs_menus[-1].addAction(ac) self.switch_menu.addMenu(self.cs_menus[-1]) self.switch_menu.addSeparator() for ac in switch_actions: self.switch_menu.addAction(ac) if self.switch_menu != self.library_action.menu(): self.library_action.setMenu(self.switch_menu) def _location_selected(self, location, *args): if location != self.current_location and hasattr( self, 'location_' + location): self.current_location = location self.location_selected.emit(location) getattr(self, 'location_' + location).setChecked(True) def _eject_requested(self, *args): self.unmount_device.emit() def _configure_requested(self): self.configure_device.emit() def update_devices(self, cp=(None, None), fs=[-1, -1, -1], icon=None): if icon is None: icon = I('reader.png') self.location_main.setIcon(QIcon(icon)) had_device = self.has_device if cp is None: cp = (None, None) if isinstance(cp, (str, unicode)): cp = (cp, None) if len(fs) < 3: fs = list(fs) + [0] self.free[0] = fs[0] self.free[1] = fs[1] self.free[2] = fs[2] cpa, cpb = cp self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1 self.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1 self.update_tooltips() if self.has_device != had_device: self.location_library.setChecked(True) self.locations_changed.emit() if not self.has_device: self.location_library.trigger() def update_tooltips(self): for i, loc in enumerate(('main', 'carda', 'cardb')): t = self.tooltips[loc] if self.free[i] > -1: t += u'\n\n%s ' % human_readable(self.free[i]) + _('available') ac = getattr(self, 'location_' + loc) ac.setToolTip(t) ac.setWhatsThis(t) ac.setStatusTip(t) @property def has_device(self): return max(self.free) > -1 @property def available_actions(self): ans = [self.location_library] for i, loc in enumerate(('main', 'carda', 'cardb')): if self.free[i] > -1: ans.append(getattr(self, 'location_' + loc)) return ans
class TagsView(QTreeView): # {{{ refresh_required = pyqtSignal() tags_marked = pyqtSignal(object) edit_user_category = pyqtSignal(object) delete_user_category = pyqtSignal(object) del_item_from_user_cat = pyqtSignal(object, object, object) add_item_to_user_cat = pyqtSignal(object, object, object) add_subcategory = pyqtSignal(object) tags_list_edit = pyqtSignal(object, object) saved_search_edit = pyqtSignal(object) rebuild_saved_searches = pyqtSignal() author_sort_edit = pyqtSignal(object, object, object, object) tag_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal() drag_drop_finished = pyqtSignal(object) restriction_error = pyqtSignal() tag_item_delete = pyqtSignal(object, object, object) def __init__(self, parent=None): QTreeView.__init__(self, parent=None) self.alter_tb = None self.disable_recounting = False self.setUniformRowHeights(True) self.setCursor(Qt.PointingHandCursor) self.setIconSize(QSize(20, 20)) self.setTabKeyNavigation(True) self.setAnimated(True) self.setHeaderHidden(True) self.setItemDelegate(TagDelegate(self)) self.made_connections = False self.setAcceptDrops(True) self.setDragEnabled(True) self.setDragDropMode(self.DragDrop) self.setDropIndicatorShown(True) self.in_drag_drop = False self.setAutoExpandDelay(500) self.pane_is_visible = False self.search_icon = QIcon(I('search.png')) self.user_category_icon = QIcon(I('tb_folder.png')) self.delete_icon = QIcon(I('list_remove.png')) self.rename_icon = QIcon(I('edit-undo.png')) self._model = TagsModel(self) self._model.search_item_renamed.connect(self.search_item_renamed) self._model.refresh_required.connect(self.refresh_required, type=Qt.QueuedConnection) self._model.tag_item_renamed.connect(self.tag_item_renamed) self._model.restriction_error.connect(self.restriction_error) self._model.user_categories_edited.connect(self.user_categories_edited, type=Qt.QueuedConnection) self._model.drag_drop_finished.connect(self.drag_drop_finished) stylish_tb = ''' QTreeView { background-color: palette(window); color: palette(window-text); border: none; } ''' self.setStyleSheet(''' QTreeView::item { border: 1px solid transparent; padding-top:0.9ex; padding-bottom:0.9ex; } QTreeView::item:hover { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1); border: 1px solid #bfcde4; border-radius: 6px; } ''' + ('' if gprefs['tag_browser_old_look'] else stylish_tb)) if gprefs['tag_browser_old_look']: self.setAlternatingRowColors(True) @property def hidden_categories(self): return self._model.hidden_categories @property def db(self): return self._model.db @property def collapse_model(self): return self._model.collapse_model def set_pane_is_visible(self, to_what): pv = self.pane_is_visible self.pane_is_visible = to_what if to_what and not pv: self.recount() def get_state(self): state_map = {} expanded_categories = [] for row, category in enumerate(self._model.category_nodes): if self.isExpanded(self._model.index(row, 0, QModelIndex())): expanded_categories.append(category.category_key) states = [c.tag.state for c in category.child_tags()] names = [(c.tag.name, c.tag.category) for c in category.child_tags()] state_map[category.category_key] = dict(izip(names, states)) return expanded_categories, state_map def reread_collapse_parameters(self): self._model.reread_collapse_model(self.get_state()[1]) def set_database(self, db, alter_tb): self._model.set_database(db) self.alter_tb = alter_tb self.pane_is_visible = True # because TagsModel.set_database did a recount self.setModel(self._model) self.setContextMenuPolicy(Qt.CustomContextMenu) pop = self.db.CATEGORY_SORTS.index(config['sort_tags_by']) self.alter_tb.sort_menu.actions()[pop].setChecked(True) try: match_pop = self.db.MATCH_TYPE.index(config['match_tags_type']) except ValueError: match_pop = 0 self.alter_tb.match_menu.actions()[match_pop].setChecked(True) if not self.made_connections: self.clicked.connect(self.toggle) self.customContextMenuRequested.connect(self.show_context_menu) self.refresh_required.connect(self.recount, type=Qt.QueuedConnection) self.alter_tb.sort_menu.triggered.connect(self.sort_changed) self.alter_tb.match_menu.triggered.connect(self.match_changed) self.made_connections = True self.refresh_signal_processed = True db.add_listener(self.database_changed) self.expanded.connect(self.item_expanded) def database_changed(self, event, ids): if self.refresh_signal_processed: self.refresh_signal_processed = False self.refresh_required.emit() def user_categories_edited(self, user_cats, nkey): state_map = self.get_state()[1] self.db.prefs.set('user_categories', user_cats) self._model.rebuild_node_tree(state_map=state_map) p = self._model.find_category_node('@'+nkey) self.show_item_at_path(p) @property def match_all(self): return (self.alter_tb and self.alter_tb.match_menu.actions()[1].isChecked()) def sort_changed(self, action): for i, ac in enumerate(self.alter_tb.sort_menu.actions()): if ac is action: config.set('sort_tags_by', self.db.CATEGORY_SORTS[i]) self.recount() break def match_changed(self, action): try: for i, ac in enumerate(self.alter_tb.match_menu.actions()): if ac is action: config.set('match_tags_type', self.db.MATCH_TYPE[i]) except: pass def mouseMoveEvent(self, event): dex = self.indexAt(event.pos()) if self.in_drag_drop or not dex.isValid(): QTreeView.mouseMoveEvent(self, event) return # Must deal with odd case where the node being dragged is 'virtual', # created to form a hierarchy. We can't really drag this node, but in # addition we can't allow drag recognition to notice going over some # other node and grabbing that one. So we set in_drag_drop to prevent # this from happening, turning it off when the user lifts the button. self.in_drag_drop = True if not self._model.flags(dex) & Qt.ItemIsDragEnabled: QTreeView.mouseMoveEvent(self, event) return md = self._model.mimeData([dex]) pixmap = dex.data(Qt.DecorationRole).toPyObject().pixmap(25, 25) drag = QDrag(self) drag.setPixmap(pixmap) drag.setMimeData(md) if self._model.is_in_user_category(dex): drag.exec_(Qt.CopyAction|Qt.MoveAction, Qt.CopyAction) else: drag.exec_(Qt.CopyAction) def mouseReleaseEvent(self, event): # Swallow everything except leftButton so context menus work correctly if event.button() == Qt.LeftButton or self.in_drag_drop: QTreeView.mouseReleaseEvent(self, event) self.in_drag_drop = False def mouseDoubleClickEvent(self, event): # swallow these to avoid toggling and editing at the same time pass @property def search_string(self): tokens = self._model.tokens() joiner = ' and ' if self.match_all else ' or ' return joiner.join(tokens) def toggle(self, index): self._toggle(index, None) def _toggle(self, index, set_to): ''' set_to: if None, advance the state. Otherwise must be one of the values in TAG_SEARCH_STATES ''' modifiers = int(QApplication.keyboardModifiers()) exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT) if self._model.toggle(index, exclusive, set_to=set_to): self.tags_marked.emit(self.search_string) def conditional_clear(self, search_string): if search_string != self.search_string: self.clear() def context_menu_handler(self, action=None, category=None, key=None, index=None, search_state=None): if not action: return try: if action == 'set_icon': try: path = choose_files(self, 'choose_category_icon', _('Change Icon for: %s')%key, filters=[ ('Images', ['png', 'gif', 'jpg', 'jpeg'])], all_files=False, select_only_single_file=True) if path: path = path[0] p = QIcon(path).pixmap(QSize(128, 128)) d = os.path.join(config_dir, 'tb_icons') if not os.path.exists(d): os.makedirs(d) with open(os.path.join(d, 'icon_'+ sanitize_file_name_unicode(key)+'.png'), 'wb') as f: f.write(pixmap_to_data(p, format='PNG')) path = os.path.basename(f.name) self._model.set_custom_category_icon(key, unicode(path)) self.recount() except: import traceback traceback.print_exc() return if action == 'clear_icon': self._model.set_custom_category_icon(key, None) self.recount() return if action == 'edit_item': self.edit(index) return if action == 'delete_item': self.tag_item_delete.emit(key, index.id, index.original_name) return if action == 'open_editor': self.tags_list_edit.emit(category, key) return if action == 'manage_categories': self.edit_user_category.emit(category) return if action == 'search': self._toggle(index, set_to=search_state) return if action == 'add_to_category': tag = index.tag if len(index.children) > 0: for c in index.all_children(): self.add_item_to_user_cat.emit(category, c.tag.original_name, c.tag.category) self.add_item_to_user_cat.emit(category, tag.original_name, tag.category) return if action == 'add_subcategory': self.add_subcategory.emit(key) return if action == 'search_category': self._toggle(index, set_to=search_state) return if action == 'delete_user_category': self.delete_user_category.emit(key) return if action == 'delete_search': saved_searches().delete(key) self.rebuild_saved_searches.emit() return if action == 'delete_item_from_user_category': tag = index.tag if len(index.children) > 0: for c in index.children: self.del_item_from_user_cat.emit(key, c.tag.original_name, c.tag.category) self.del_item_from_user_cat.emit(key, tag.original_name, tag.category) return if action == 'manage_searches': self.saved_search_edit.emit(category) return if action == 'edit_author_sort': self.author_sort_edit.emit(self, index, True, False) return if action == 'edit_author_link': self.author_sort_edit.emit(self, index, False, True) return reset_filter_categories = True if action == 'hide': self.hidden_categories.add(category) elif action == 'show': self.hidden_categories.discard(category) elif action == 'categorization': changed = self.collapse_model != category self._model.collapse_model = category if changed: reset_filter_categories = False gprefs['tags_browser_partition_method'] = category elif action == 'defaults': self.hidden_categories.clear() self.db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories)) if reset_filter_categories: self._model.set_categories_filter(None) self._model.rebuild_node_tree() except: return def show_context_menu(self, point): def display_name( tag): if tag.category == 'search': n = tag.name if len(n) > 45: n = n[:45] + '...' return "'" + n + "'" return tag.name index = self.indexAt(point) self.context_menu = QMenu(self) if index.isValid(): item = index.data(Qt.UserRole).toPyObject() tag = None if item.type == TagTreeItem.TAG: tag_item = item tag = item.tag while item.type != TagTreeItem.CATEGORY: item = item.parent if item.type == TagTreeItem.CATEGORY: if not item.category_key.startswith('@'): while item.parent != self._model.root_item: item = item.parent category = unicode(item.name.toString()) key = item.category_key # Verify that we are working with a field that we know something about if key not in self.db.field_metadata: return True # Did the user click on a leaf node? if tag: # If the user right-clicked on an editable item, then offer # the possibility of renaming that item. if tag.is_editable: # Add the 'rename' items self.context_menu.addAction(self.rename_icon, _('Rename %s')%display_name(tag), partial(self.context_menu_handler, action='edit_item', index=index)) if key in ('tags', 'series', 'publisher') or \ self._model.db.field_metadata.is_custom_field(key): self.context_menu.addAction(self.delete_icon, _('Delete %s')%display_name(tag), partial(self.context_menu_handler, action='delete_item', key=key, index=tag)) if key == 'authors': self.context_menu.addAction(_('Edit sort for %s')%display_name(tag), partial(self.context_menu_handler, action='edit_author_sort', index=tag.id)) self.context_menu.addAction(_('Edit link for %s')%display_name(tag), partial(self.context_menu_handler, action='edit_author_link', index=tag.id)) # is_editable is also overloaded to mean 'can be added # to a user category' m = self.context_menu.addMenu(self.user_category_icon, _('Add %s to user category')%display_name(tag)) nt = self.model().category_node_tree def add_node_tree(tree_dict, m, path): p = path[:] for k in sorted(tree_dict.keys(), key=sort_key): p.append(k) n = k[1:] if k.startswith('@') else k m.addAction(self.user_category_icon, n, partial(self.context_menu_handler, 'add_to_category', category='.'.join(p), index=tag_item)) if len(tree_dict[k]): tm = m.addMenu(self.user_category_icon, _('Children of %s')%n) add_node_tree(tree_dict[k], tm, p) p.pop() add_node_tree(nt, m, []) elif key == 'search' and tag.is_searchable: self.context_menu.addAction(self.rename_icon, _('Rename %s')%display_name(tag), partial(self.context_menu_handler, action='edit_item', index=index)) self.context_menu.addAction(self.delete_icon, _('Delete search %s')%display_name(tag), partial(self.context_menu_handler, action='delete_search', key=tag.original_name)) if key.startswith('@') and not item.is_gst: self.context_menu.addAction(self.user_category_icon, _('Remove %(item)s from category %(cat)s')% dict(item=display_name(tag), cat=item.py_name), partial(self.context_menu_handler, action='delete_item_from_user_category', key = key, index = tag_item)) if tag.is_searchable: # Add the search for value items. All leaf nodes are searchable self.context_menu.addAction(self.search_icon, _('Search for %s')%display_name(tag), partial(self.context_menu_handler, action='search', search_state=TAG_SEARCH_STATES['mark_plus'], index=index)) self.context_menu.addAction(self.search_icon, _('Search for everything but %s')%display_name(tag), partial(self.context_menu_handler, action='search', search_state=TAG_SEARCH_STATES['mark_minus'], index=index)) self.context_menu.addSeparator() elif key.startswith('@') and not item.is_gst: if item.can_be_edited: self.context_menu.addAction(self.rename_icon, _('Rename %s')%item.py_name, partial(self.context_menu_handler, action='edit_item', index=index)) self.context_menu.addAction(self.user_category_icon, _('Add sub-category to %s')%item.py_name, partial(self.context_menu_handler, action='add_subcategory', key=key)) self.context_menu.addAction(self.delete_icon, _('Delete user category %s')%item.py_name, partial(self.context_menu_handler, action='delete_user_category', key=key)) self.context_menu.addSeparator() # Hide/Show/Restore categories self.context_menu.addAction(_('Hide category %s') % category, partial(self.context_menu_handler, action='hide', category=key)) if self.hidden_categories: m = self.context_menu.addMenu(_('Show category')) for col in sorted(self.hidden_categories, key=lambda x: sort_key(self.db.field_metadata[x]['name'])): m.addAction(self.db.field_metadata[col]['name'], partial(self.context_menu_handler, action='show', category=col)) # search by category. Some categories are not searchable, such # as search and news if item.tag.is_searchable: self.context_menu.addAction(self.search_icon, _('Search for books in category %s')%category, partial(self.context_menu_handler, action='search_category', index=self._model.createIndex(item.row(), 0, item), search_state=TAG_SEARCH_STATES['mark_plus'])) self.context_menu.addAction(self.search_icon, _('Search for books not in category %s')%category, partial(self.context_menu_handler, action='search_category', index=self._model.createIndex(item.row(), 0, item), search_state=TAG_SEARCH_STATES['mark_minus'])) # Offer specific editors for tags/series/publishers/saved searches self.context_menu.addSeparator() if key in ['tags', 'publisher', 'series'] or \ (self.db.field_metadata[key]['is_custom'] and self.db.field_metadata[key]['datatype'] != 'composite'): self.context_menu.addAction(_('Manage %s')%category, partial(self.context_menu_handler, action='open_editor', category=tag.original_name if tag else None, key=key)) elif key == 'authors': self.context_menu.addAction(_('Manage %s')%category, partial(self.context_menu_handler, action='edit_author_sort')) elif key == 'search': self.context_menu.addAction(_('Manage Saved Searches'), partial(self.context_menu_handler, action='manage_searches', category=tag.name if tag else None)) self.context_menu.addSeparator() self.context_menu.addAction(_('Change category icon'), partial(self.context_menu_handler, action='set_icon', key=key)) self.context_menu.addAction(_('Restore default icon'), partial(self.context_menu_handler, action='clear_icon', key=key)) # Always show the user categories editor self.context_menu.addSeparator() if key.startswith('@') and \ key[1:] in self.db.prefs.get('user_categories', {}).keys(): self.context_menu.addAction(_('Manage User Categories'), partial(self.context_menu_handler, action='manage_categories', category=key[1:])) else: self.context_menu.addAction(_('Manage User Categories'), partial(self.context_menu_handler, action='manage_categories', category=None)) if self.hidden_categories: if not self.context_menu.isEmpty(): self.context_menu.addSeparator() self.context_menu.addAction(_('Show all categories'), partial(self.context_menu_handler, action='defaults')) m = self.context_menu.addMenu(_('Change sub-categorization scheme')) da = m.addAction(_('Disable'), partial(self.context_menu_handler, action='categorization', category='disable')) fla = m.addAction(_('By first letter'), partial(self.context_menu_handler, action='categorization', category='first letter')) pa = m.addAction(_('Partition'), partial(self.context_menu_handler, action='categorization', category='partition')) if self.collapse_model == 'disable': da.setCheckable(True) da.setChecked(True) elif self.collapse_model == 'first letter': fla.setCheckable(True) fla.setChecked(True) else: pa.setCheckable(True) pa.setChecked(True) if config['sort_tags_by'] != "name": fla.setEnabled(False) m.hovered.connect(self.collapse_menu_hovered) fla.setToolTip(_('First letter is usable only when sorting by name')) # Apparently one cannot set a tooltip to empty, so use a star and # deal with it in the hover method da.setToolTip('*') pa.setToolTip('*') if not self.context_menu.isEmpty(): self.context_menu.popup(self.mapToGlobal(point)) return True def collapse_menu_hovered(self, action): tip = action.toolTip() if tip == '*': tip = '' QToolTip.showText(QCursor.pos(), tip) def dragMoveEvent(self, event): QTreeView.dragMoveEvent(self, event) self.setDropIndicatorShown(False) index = self.indexAt(event.pos()) if not index.isValid(): return src_is_tb = event.mimeData().hasFormat('application/calibre+from_tag_browser') item = index.data(Qt.UserRole).toPyObject() if item.type == TagTreeItem.ROOT: return flags = self._model.flags(index) if item.type == TagTreeItem.TAG and flags & Qt.ItemIsDropEnabled: self.setDropIndicatorShown(not src_is_tb) return if item.type == TagTreeItem.CATEGORY and not item.is_gst: fm_dest = self.db.metadata_for_field(item.category_key) if fm_dest['kind'] == 'user': if src_is_tb: if event.dropAction() == Qt.MoveAction: data = str(event.mimeData().data('application/calibre+from_tag_browser')) src = cPickle.loads(data) for s in src: if s[0] == TagTreeItem.TAG and \ (not s[1].startswith('@') or s[2]): return self.setDropIndicatorShown(True) return md = event.mimeData() if hasattr(md, 'column_name'): fm_src = self.db.metadata_for_field(md.column_name) if md.column_name in ['authors', 'publisher', 'series'] or \ (fm_src['is_custom'] and ( (fm_src['datatype'] in ['series', 'text', 'enumeration'] and not fm_src['is_multiple']) or (fm_src['datatype'] == 'composite' and fm_src['display'].get('make_category', False)))): self.setDropIndicatorShown(True) def clear(self): if self.model(): self.model().clear_state() def is_visible(self, idx): item = idx.data(Qt.UserRole).toPyObject() if getattr(item, 'type', None) == TagTreeItem.TAG: idx = idx.parent() return self.isExpanded(idx) def recount(self, *args): ''' Rebuild the category tree, expand any categories that were expanded, reset the search states, and reselect the current node. ''' if self.disable_recounting or not self.pane_is_visible: return self.refresh_signal_processed = True ci = self.currentIndex() if not ci.isValid(): ci = self.indexAt(QPoint(10, 10)) path = self.model().path_for_index(ci) if self.is_visible(ci) else None expanded_categories, state_map = self.get_state() self._model.rebuild_node_tree(state_map=state_map) self.blockSignals(True) for category in expanded_categories: idx = self._model.index_for_category(category) if idx is not None and idx.isValid(): self.expand(idx) self.show_item_at_path(path) self.blockSignals(False) def show_item_at_path(self, path, box=False, position=QTreeView.PositionAtCenter): ''' Scroll the browser and open categories to show the item referenced by path. If possible, the item is placed in the center. If box=True, a box is drawn around the item. ''' if path: self.show_item_at_index(self._model.index_for_path(path), box=box, position=position) def expand_parent(self, idx): # Needed otherwise Qt sometimes segfaults if the node is buried in a # collapsed, off screen hierarchy. To be safe, we expand from the # outermost in p = self._model.parent(idx) if p.isValid(): self.expand_parent(p) self.expand(idx) def show_item_at_index(self, idx, box=False, position=QTreeView.PositionAtCenter): if idx.isValid() and idx.data(Qt.UserRole).toPyObject() is not self._model.root_item: self.expand_parent(idx) self.setCurrentIndex(idx) self.scrollTo(idx, position) if box: self._model.set_boxed(idx) def item_expanded(self, idx): ''' Called by the expanded signal ''' self.setCurrentIndex(idx)
class GEditorMenuBar(QMenuBar): def __init__(self, parent): QMenuBar.__init__(self, parent) # Menu items self._fileM = QMenu(self.tr("File")) self._editM = QMenu(self.tr("Edit")) self._viewM = QMenu(self.tr("View")) self._optionM = QMenu(self.tr("Options")) self.addMenu(self._fileM) self.addMenu(self._editM) self.addMenu(self._viewM) self.addMenu(self._optionM) # Actions self._connectDatabaseA = QAction(self.tr("Connect a PostGIS database"), self.parent()) self._connectDatabaseA.setShortcut(QKeySequence("Ctrl+Shift+C")) self.parent().connect(self._connectDatabaseA, SIGNAL("triggered()"), self.parent().dialConnectDatabase) self._importDatabaseDataA = QAction( self.tr("Import data from database"), self.parent()) self._importDatabaseDataA.setShortcut(QKeySequence("Ctrl+Shift+I")) self.parent().connect(self._importDatabaseDataA, SIGNAL("triggered()"), self.parent().dialImportDatabaseData) self._importVectorFileDataA = QAction( self.tr("Import vector from file"), self.parent()) self._importVectorFileDataA.setShortcut(QKeySequence("Ctrl+Shift+V")) self.parent().connect(self._importVectorFileDataA, SIGNAL("triggered()"), self.parent().dialImportVectorFileData) self._importRasterFileDataA = QAction( self.tr("Import raster data from file"), self.parent()) self._importRasterFileDataA.setShortcut(QKeySequence("Ctrl+Shift+R")) self.parent().connect(self._importRasterFileDataA, SIGNAL("triggered()"), self.parent().dialImportRasterFileData) self._colorLayerA = QAction(self.tr("Select layer color"), self.parent()) self.parent().connect(self._colorLayerA, SIGNAL("triggered()"), self.parent().dialSelectLayerColor) # Add actions to menu self._fileM.addAction(self._importVectorFileDataA) self._fileM.addAction(self._importRasterFileDataA) self._fileM.addSeparator() self._fileM.addAction(self._connectDatabaseA) self._fileM.addAction(self._importDatabaseDataA) self._viewM.addAction(self._colorLayerA) def dial(self): dial = QDialog(self) dial.setWindowTitle("Select a layer") layerList = QListWidget(dial) db = GPostGISDatabase() for t in db.tables(): layerList.addItem(t) dial.setBaseSize(layerList.minimumSize()) dial.show()
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) d.exec_() d.break_cycles() 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
def show_context_menu(self, pos): m = QMenu(self) a = m.addAction c = self.editor.cursorForPosition(pos) origc = QTextCursor(c) r = origr = self.editor.syntax_range_for_cursor(c) if (r is None or not r.format.property(SPELL_PROPERTY).toBool() ) and c.positionInBlock() > 0: c.setPosition(c.position() - 1) r = self.editor.syntax_range_for_cursor(c) if r is not None and r.format.property(SPELL_PROPERTY).toBool(): word = self.editor.text_for_range(c.block(), r) locale = self.editor.spellcheck_locale_for_cursor(c) orig_pos = c.position() c.setPosition(orig_pos - utf16_length(word)) found = False self.editor.setTextCursor(c) if self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False): found = True fc = self.editor.textCursor() if fc.position() < c.position(): self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False) if found: suggestions = dictionaries.suggestions(word, locale)[:7] if suggestions: for suggestion in suggestions: ac = m.addAction( suggestion, partial(self.editor.simple_replace, suggestion)) f = ac.font() f.setBold(True), ac.setFont(f) m.addSeparator() m.addAction(actions['spell-next']) m.addAction(_('Ignore this word'), partial(self._nuke_word, None, word, locale)) dics = dictionaries.active_user_dictionaries if len(dics) > 0: if len(dics) == 1: m.addAction( _('Add this word to the dictionary: {0}').format( dics[0].name), partial(self._nuke_word, dics[0].name, word, locale)) else: ac = m.addAction(_('Add this word to the dictionary')) dmenu = QMenu(m) ac.setMenu(dmenu) for dic in dics: dmenu.addAction( dic.name, partial(self._nuke_word, dic.name, word, locale)) m.addSeparator() if origr is not None and origr.format.property(LINK_PROPERTY).toBool(): href = self.editor.text_for_range(origc.block(), origr) m.addAction( _('Open %s') % href, partial(self.link_clicked.emit, href)) if origr is not None and ( origr.format.property(TAG_NAME_PROPERTY).toBool() or origr.format.property(CSS_PROPERTY).toBool()): word = self.editor.text_for_range(origc.block(), origr) item_type = 'tag_name' if origr.format.property( TAG_NAME_PROPERTY).toBool() else 'css_property' url = help_url(word, item_type, self.editor.highlighter.doc_name, extra_data=current_container().opf_version) if url is not None: m.addAction( _('Show help for: %s') % word, partial(open_url, url)) for x in ('undo', 'redo'): a(actions['editor-%s' % x]) m.addSeparator() for x in ('cut', 'copy', 'paste'): a(actions['editor-' + x]) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) m.addAction(actions['mark-selected-text']) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.mapToGlobal(pos))
def __init__(self, parent, hide_on_close=False): QMainWindow.__init__(self, parent) self.setWindowIcon(pixmaps.tigger_starface.icon()) self._currier = PersistentCurrier() self.hide() # init column constants for icol, col in enumerate(self.ViewModelColumns): setattr(self, "Column%s" % col.capitalize(), icol) # init GUI self.setWindowTitle("Tigger") # self.setIcon(pixmaps.purr_logo.pm()) cw = QWidget(self) self.setCentralWidget(cw) cwlo = QVBoxLayout(cw) cwlo.setMargin(5) # make splitter spl1 = self._splitter1 = QSplitter(Qt.Vertical, cw) spl1.setOpaqueResize(False) cwlo.addWidget(spl1) # Create listview of LSM entries self.tw = SkyModelTreeWidget(spl1) self.tw.hide() # split bottom pane spl2 = self._splitter2 = QSplitter(Qt.Horizontal, spl1) spl2.setOpaqueResize(False) self._skyplot_stack = QWidget(spl2) self._skyplot_stack_lo = QVBoxLayout(self._skyplot_stack) self._skyplot_stack_lo.setContentsMargins(0, 0, 0, 0) # add plot self.skyplot = SkyModelPlotter(self._skyplot_stack, self) self.skyplot.resize(128, 128) self.skyplot.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) self._skyplot_stack_lo.addWidget(self.skyplot, 1000) self.skyplot.hide() QObject.connect(self.skyplot, SIGNAL("imagesChanged"), self._imagesChanged) QObject.connect(self.skyplot, SIGNAL("showMessage"), self.showMessage) QObject.connect(self.skyplot, SIGNAL("showErrorMessage"), self.showErrorMessage) self._grouptab_stack = QWidget(spl2) self._grouptab_stack_lo = lo = QVBoxLayout(self._grouptab_stack) self._grouptab_stack_lo.setContentsMargins(0, 0, 0, 0) # add groupings table self.grouptab = ModelGroupsTable(self._grouptab_stack) self.grouptab.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) QObject.connect(self, SIGNAL("hasSkyModel"), self.grouptab.setEnabled) lo.addWidget(self.grouptab, 1000) lo.addStretch(1) self.grouptab.hide() # add image controls -- parentless for now (setLayout will reparent them anyway) self.imgman = ImageManager() self.skyplot.setImageManager(self.imgman) QObject.connect(self.imgman, SIGNAL("imagesChanged"), self._imagesChanged) QObject.connect(self.imgman, SIGNAL("showMessage"), self.showMessage) QObject.connect(self.imgman, SIGNAL("showErrorMessage"), self.showErrorMessage) # enable status line self.statusBar().show() # Create and populate main menu menubar = self.menuBar() # File menu file_menu = menubar.addMenu("&File") qa_open = file_menu.addAction("&Open model...", self._openFileCallback, Qt.CTRL + Qt.Key_O) qa_merge = file_menu.addAction("&Merge in model...", self._mergeFileCallback, Qt.CTRL + Qt.SHIFT + Qt.Key_O) QObject.connect(self, SIGNAL("hasSkyModel"), qa_merge.setEnabled) file_menu.addSeparator() qa_save = file_menu.addAction("&Save model", self.saveFile, Qt.CTRL + Qt.Key_S) QObject.connect(self, SIGNAL("isUpdated"), qa_save.setEnabled) qa_save_as = file_menu.addAction("Save model &as...", self.saveFileAs) QObject.connect(self, SIGNAL("hasSkyModel"), qa_save_as.setEnabled) qa_save_selection_as = file_menu.addAction("Save selection as...", self.saveSelectionAs) QObject.connect(self, SIGNAL("hasSelection"), qa_save_selection_as.setEnabled) file_menu.addSeparator() qa_close = file_menu.addAction("&Close model", self.closeFile, Qt.CTRL + Qt.Key_W) QObject.connect(self, SIGNAL("hasSkyModel"), qa_close.setEnabled) qa_quit = file_menu.addAction("Quit", self.close, Qt.CTRL + Qt.Key_Q) # Image menu menubar.addMenu(self.imgman.getMenu()) # Plot menu menubar.addMenu(self.skyplot.getMenu()) # LSM Menu em = QMenu("&LSM", self) self._qa_em = menubar.addMenu(em) self._qa_em.setVisible(False) QObject.connect(self, SIGNAL("hasSkyModel"), self._qa_em.setVisible) self._column_view_menu = QMenu("&Show columns", self) self._qa_cv_menu = em.addMenu(self._column_view_menu) em.addSeparator() em.addAction("Select &all", self._selectAll, Qt.CTRL + Qt.Key_A) em.addAction("&Invert selection", self._selectInvert, Qt.CTRL + Qt.Key_I) em.addAction("Select b&y attribute...", self._showSourceSelector, Qt.CTRL + Qt.Key_Y) em.addSeparator() qa_add_tag = em.addAction("&Tag selection...", self.addTagToSelection, Qt.CTRL + Qt.Key_T) QObject.connect(self, SIGNAL("hasSelection"), qa_add_tag.setEnabled) qa_del_tag = em.addAction("&Untag selection...", self.removeTagsFromSelection, Qt.CTRL + Qt.Key_U) QObject.connect(self, SIGNAL("hasSelection"), qa_del_tag.setEnabled) qa_del_sel = em.addAction("&Delete selection", self._deleteSelection) QObject.connect(self, SIGNAL("hasSelection"), qa_del_sel.setEnabled) # Tools menu tm = self._tools_menu = QMenu("&Tools", self) self._qa_tm = menubar.addMenu(tm) self._qa_tm.setVisible(False) QObject.connect(self, SIGNAL("hasSkyModel"), self._qa_tm.setVisible) # Help menu menubar.addSeparator() hm = self._help_menu = menubar.addMenu("&Help") hm.addAction("&About...", self._showAboutDialog) self._about_dialog = None # message handlers self.qerrmsg = QErrorMessage(self) # set initial state self.setAcceptDrops(True) self.model = None self.filename = None self._display_filename = None self._open_file_dialog = self._merge_file_dialog = self._save_as_dialog = self._save_sel_as_dialog = self._open_image_dialog = None self.emit(SIGNAL("isUpdated"), False) self.emit(SIGNAL("hasSkyModel"), False) self.emit(SIGNAL("hasSelection"), False) self._exiting = False # set initial layout self._current_layout = None self.setLayout(self.LayoutEmpty) dprint(1, "init complete")
class TagsView(QTreeView): # {{{ refresh_required = pyqtSignal() tags_marked = pyqtSignal(object) edit_user_category = pyqtSignal(object) delete_user_category = pyqtSignal(object) del_item_from_user_cat = pyqtSignal(object, object, object) add_item_to_user_cat = pyqtSignal(object, object, object) add_subcategory = pyqtSignal(object) tags_list_edit = pyqtSignal(object, object) saved_search_edit = pyqtSignal(object) rebuild_saved_searches = pyqtSignal() author_sort_edit = pyqtSignal(object, object, object, object) tag_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal() drag_drop_finished = pyqtSignal(object) restriction_error = pyqtSignal() tag_item_delete = pyqtSignal(object, object, object) def __init__(self, parent=None): QTreeView.__init__(self, parent=None) self.alter_tb = None self.disable_recounting = False self.setUniformRowHeights(True) self.setCursor(Qt.PointingHandCursor) self.setIconSize(QSize(20, 20)) self.setTabKeyNavigation(True) self.setAnimated(True) self.setHeaderHidden(True) self.setItemDelegate(TagDelegate(self)) self.made_connections = False self.setAcceptDrops(True) self.setDragEnabled(True) self.setDragDropMode(self.DragDrop) self.setDropIndicatorShown(True) self.in_drag_drop = False self.setAutoExpandDelay(500) self.pane_is_visible = False self.search_icon = QIcon(I('search.png')) self.user_category_icon = QIcon(I('tb_folder.png')) self.delete_icon = QIcon(I('list_remove.png')) self.rename_icon = QIcon(I('edit-undo.png')) self._model = TagsModel(self) self._model.search_item_renamed.connect(self.search_item_renamed) self._model.refresh_required.connect(self.refresh_required, type=Qt.QueuedConnection) self._model.tag_item_renamed.connect(self.tag_item_renamed) self._model.restriction_error.connect(self.restriction_error) self._model.user_categories_edited.connect(self.user_categories_edited, type=Qt.QueuedConnection) self._model.drag_drop_finished.connect(self.drag_drop_finished) stylish_tb = ''' QTreeView { background-color: palette(window); color: palette(window-text); border: none; } ''' self.setStyleSheet(''' QTreeView::item { border: 1px solid transparent; padding-top:0.9ex; padding-bottom:0.9ex; } QTreeView::item:hover { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1); border: 1px solid #bfcde4; border-radius: 6px; } ''' + ('' if gprefs['tag_browser_old_look'] else stylish_tb)) if gprefs['tag_browser_old_look']: self.setAlternatingRowColors(True) @property def hidden_categories(self): return self._model.hidden_categories @property def db(self): return self._model.db @property def collapse_model(self): return self._model.collapse_model def set_pane_is_visible(self, to_what): pv = self.pane_is_visible self.pane_is_visible = to_what if to_what and not pv: self.recount() def get_state(self): state_map = {} expanded_categories = [] for row, category in enumerate(self._model.category_nodes): if self.isExpanded(self._model.index(row, 0, QModelIndex())): expanded_categories.append(category.category_key) states = [c.tag.state for c in category.child_tags()] names = [(c.tag.name, c.tag.category) for c in category.child_tags()] state_map[category.category_key] = dict(izip(names, states)) return expanded_categories, state_map def reread_collapse_parameters(self): self._model.reread_collapse_model(self.get_state()[1]) def set_database(self, db, alter_tb): self._model.set_database(db) self.alter_tb = alter_tb self.pane_is_visible = True # because TagsModel.set_database did a recount self.setModel(self._model) self.setContextMenuPolicy(Qt.CustomContextMenu) pop = self.db.CATEGORY_SORTS.index(config['sort_tags_by']) self.alter_tb.sort_menu.actions()[pop].setChecked(True) try: match_pop = self.db.MATCH_TYPE.index(config['match_tags_type']) except ValueError: match_pop = 0 self.alter_tb.match_menu.actions()[match_pop].setChecked(True) if not self.made_connections: self.clicked.connect(self.toggle) self.customContextMenuRequested.connect(self.show_context_menu) self.refresh_required.connect(self.recount, type=Qt.QueuedConnection) self.alter_tb.sort_menu.triggered.connect(self.sort_changed) self.alter_tb.match_menu.triggered.connect(self.match_changed) self.made_connections = True self.refresh_signal_processed = True db.add_listener(self.database_changed) self.expanded.connect(self.item_expanded) def database_changed(self, event, ids): if self.refresh_signal_processed: self.refresh_signal_processed = False self.refresh_required.emit() def user_categories_edited(self, user_cats, nkey): state_map = self.get_state()[1] self.db.prefs.set('user_categories', user_cats) self._model.rebuild_node_tree(state_map=state_map) p = self._model.find_category_node('@' + nkey) self.show_item_at_path(p) @property def match_all(self): return (self.alter_tb and self.alter_tb.match_menu.actions()[1].isChecked()) def sort_changed(self, action): for i, ac in enumerate(self.alter_tb.sort_menu.actions()): if ac is action: config.set('sort_tags_by', self.db.CATEGORY_SORTS[i]) self.recount() break def match_changed(self, action): try: for i, ac in enumerate(self.alter_tb.match_menu.actions()): if ac is action: config.set('match_tags_type', self.db.MATCH_TYPE[i]) except: pass def mouseMoveEvent(self, event): dex = self.indexAt(event.pos()) if self.in_drag_drop or not dex.isValid(): QTreeView.mouseMoveEvent(self, event) return # Must deal with odd case where the node being dragged is 'virtual', # created to form a hierarchy. We can't really drag this node, but in # addition we can't allow drag recognition to notice going over some # other node and grabbing that one. So we set in_drag_drop to prevent # this from happening, turning it off when the user lifts the button. self.in_drag_drop = True if not self._model.flags(dex) & Qt.ItemIsDragEnabled: QTreeView.mouseMoveEvent(self, event) return md = self._model.mimeData([dex]) pixmap = dex.data(Qt.DecorationRole).toPyObject().pixmap(25, 25) drag = QDrag(self) drag.setPixmap(pixmap) drag.setMimeData(md) if self._model.is_in_user_category(dex): drag.exec_(Qt.CopyAction | Qt.MoveAction, Qt.CopyAction) else: drag.exec_(Qt.CopyAction) def mouseReleaseEvent(self, event): # Swallow everything except leftButton so context menus work correctly if event.button() == Qt.LeftButton or self.in_drag_drop: QTreeView.mouseReleaseEvent(self, event) self.in_drag_drop = False def mouseDoubleClickEvent(self, event): # swallow these to avoid toggling and editing at the same time pass @property def search_string(self): tokens = self._model.tokens() joiner = ' and ' if self.match_all else ' or ' return joiner.join(tokens) def toggle(self, index): self._toggle(index, None) def _toggle(self, index, set_to): ''' set_to: if None, advance the state. Otherwise must be one of the values in TAG_SEARCH_STATES ''' modifiers = int(QApplication.keyboardModifiers()) exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT) if self._model.toggle(index, exclusive, set_to=set_to): self.tags_marked.emit(self.search_string) def conditional_clear(self, search_string): if search_string != self.search_string: self.clear() def context_menu_handler(self, action=None, category=None, key=None, index=None, search_state=None): if not action: return try: if action == 'set_icon': try: path = choose_files(self, 'choose_category_icon', _('Change Icon for: %s') % key, filters=[ ('Images', ['png', 'gif', 'jpg', 'jpeg']) ], all_files=False, select_only_single_file=True) if path: path = path[0] p = QIcon(path).pixmap(QSize(128, 128)) d = os.path.join(config_dir, 'tb_icons') if not os.path.exists(d): os.makedirs(d) with open( os.path.join( d, 'icon_' + sanitize_file_name_unicode(key) + '.png'), 'wb') as f: f.write(pixmap_to_data(p, format='PNG')) path = os.path.basename(f.name) self._model.set_custom_category_icon( key, unicode(path)) self.recount() except: import traceback traceback.print_exc() return if action == 'clear_icon': self._model.set_custom_category_icon(key, None) self.recount() return if action == 'edit_item': self.edit(index) return if action == 'delete_item': self.tag_item_delete.emit(key, index.id, index.original_name) return if action == 'open_editor': self.tags_list_edit.emit(category, key) return if action == 'manage_categories': self.edit_user_category.emit(category) return if action == 'search': self._toggle(index, set_to=search_state) return if action == 'add_to_category': tag = index.tag if len(index.children) > 0: for c in index.all_children(): self.add_item_to_user_cat.emit(category, c.tag.original_name, c.tag.category) self.add_item_to_user_cat.emit(category, tag.original_name, tag.category) return if action == 'add_subcategory': self.add_subcategory.emit(key) return if action == 'search_category': self._toggle(index, set_to=search_state) return if action == 'delete_user_category': self.delete_user_category.emit(key) return if action == 'delete_search': saved_searches().delete(key) self.rebuild_saved_searches.emit() return if action == 'delete_item_from_user_category': tag = index.tag if len(index.children) > 0: for c in index.children: self.del_item_from_user_cat.emit( key, c.tag.original_name, c.tag.category) self.del_item_from_user_cat.emit(key, tag.original_name, tag.category) return if action == 'manage_searches': self.saved_search_edit.emit(category) return if action == 'edit_author_sort': self.author_sort_edit.emit(self, index, True, False) return if action == 'edit_author_link': self.author_sort_edit.emit(self, index, False, True) return reset_filter_categories = True if action == 'hide': self.hidden_categories.add(category) elif action == 'show': self.hidden_categories.discard(category) elif action == 'categorization': changed = self.collapse_model != category self._model.collapse_model = category if changed: reset_filter_categories = False gprefs['tags_browser_partition_method'] = category elif action == 'defaults': self.hidden_categories.clear() self.db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories)) if reset_filter_categories: self._model.set_categories_filter(None) self._model.rebuild_node_tree() except: return def show_context_menu(self, point): def display_name(tag): if tag.category == 'search': n = tag.name if len(n) > 45: n = n[:45] + '...' return "'" + n + "'" return tag.name index = self.indexAt(point) self.context_menu = QMenu(self) if index.isValid(): item = index.data(Qt.UserRole).toPyObject() tag = None if item.type == TagTreeItem.TAG: tag_item = item tag = item.tag while item.type != TagTreeItem.CATEGORY: item = item.parent if item.type == TagTreeItem.CATEGORY: if not item.category_key.startswith('@'): while item.parent != self._model.root_item: item = item.parent category = unicode(item.name.toString()) key = item.category_key # Verify that we are working with a field that we know something about if key not in self.db.field_metadata: return True # Did the user click on a leaf node? if tag: # If the user right-clicked on an editable item, then offer # the possibility of renaming that item. if tag.is_editable: # Add the 'rename' items self.context_menu.addAction( self.rename_icon, _('Rename %s') % display_name(tag), partial(self.context_menu_handler, action='edit_item', index=index)) if key in ('tags', 'series', 'publisher') or \ self._model.db.field_metadata.is_custom_field(key): self.context_menu.addAction( self.delete_icon, _('Delete %s') % display_name(tag), partial(self.context_menu_handler, action='delete_item', key=key, index=tag)) if key == 'authors': self.context_menu.addAction( _('Edit sort for %s') % display_name(tag), partial(self.context_menu_handler, action='edit_author_sort', index=tag.id)) self.context_menu.addAction( _('Edit link for %s') % display_name(tag), partial(self.context_menu_handler, action='edit_author_link', index=tag.id)) # is_editable is also overloaded to mean 'can be added # to a user category' m = self.context_menu.addMenu( self.user_category_icon, _('Add %s to user category') % display_name(tag)) nt = self.model().category_node_tree def add_node_tree(tree_dict, m, path): p = path[:] for k in sorted(tree_dict.keys(), key=sort_key): p.append(k) n = k[1:] if k.startswith('@') else k m.addAction( self.user_category_icon, n, partial(self.context_menu_handler, 'add_to_category', category='.'.join(p), index=tag_item)) if len(tree_dict[k]): tm = m.addMenu(self.user_category_icon, _('Children of %s') % n) add_node_tree(tree_dict[k], tm, p) p.pop() add_node_tree(nt, m, []) elif key == 'search' and tag.is_searchable: self.context_menu.addAction( self.rename_icon, _('Rename %s') % display_name(tag), partial(self.context_menu_handler, action='edit_item', index=index)) self.context_menu.addAction( self.delete_icon, _('Delete search %s') % display_name(tag), partial(self.context_menu_handler, action='delete_search', key=tag.original_name)) if key.startswith('@') and not item.is_gst: self.context_menu.addAction( self.user_category_icon, _('Remove %(item)s from category %(cat)s') % dict(item=display_name(tag), cat=item.py_name), partial(self.context_menu_handler, action='delete_item_from_user_category', key=key, index=tag_item)) if tag.is_searchable: # Add the search for value items. All leaf nodes are searchable self.context_menu.addAction( self.search_icon, _('Search for %s') % display_name(tag), partial( self.context_menu_handler, action='search', search_state=TAG_SEARCH_STATES['mark_plus'], index=index)) self.context_menu.addAction( self.search_icon, _('Search for everything but %s') % display_name(tag), partial( self.context_menu_handler, action='search', search_state=TAG_SEARCH_STATES['mark_minus'], index=index)) self.context_menu.addSeparator() elif key.startswith('@') and not item.is_gst: if item.can_be_edited: self.context_menu.addAction( self.rename_icon, _('Rename %s') % item.py_name, partial(self.context_menu_handler, action='edit_item', index=index)) self.context_menu.addAction( self.user_category_icon, _('Add sub-category to %s') % item.py_name, partial(self.context_menu_handler, action='add_subcategory', key=key)) self.context_menu.addAction( self.delete_icon, _('Delete user category %s') % item.py_name, partial(self.context_menu_handler, action='delete_user_category', key=key)) self.context_menu.addSeparator() # Hide/Show/Restore categories self.context_menu.addAction( _('Hide category %s') % category, partial(self.context_menu_handler, action='hide', category=key)) if self.hidden_categories: m = self.context_menu.addMenu(_('Show category')) for col in sorted(self.hidden_categories, key=lambda x: sort_key( self.db.field_metadata[x]['name'])): m.addAction( self.db.field_metadata[col]['name'], partial(self.context_menu_handler, action='show', category=col)) # search by category. Some categories are not searchable, such # as search and news if item.tag.is_searchable: self.context_menu.addAction( self.search_icon, _('Search for books in category %s') % category, partial(self.context_menu_handler, action='search_category', index=self._model.createIndex( item.row(), 0, item), search_state=TAG_SEARCH_STATES['mark_plus'])) self.context_menu.addAction( self.search_icon, _('Search for books not in category %s') % category, partial(self.context_menu_handler, action='search_category', index=self._model.createIndex( item.row(), 0, item), search_state=TAG_SEARCH_STATES['mark_minus'])) # Offer specific editors for tags/series/publishers/saved searches self.context_menu.addSeparator() if key in ['tags', 'publisher', 'series'] or \ (self.db.field_metadata[key]['is_custom'] and self.db.field_metadata[key]['datatype'] != 'composite'): self.context_menu.addAction( _('Manage %s') % category, partial(self.context_menu_handler, action='open_editor', category=tag.original_name if tag else None, key=key)) elif key == 'authors': self.context_menu.addAction( _('Manage %s') % category, partial(self.context_menu_handler, action='edit_author_sort')) elif key == 'search': self.context_menu.addAction( _('Manage Saved Searches'), partial(self.context_menu_handler, action='manage_searches', category=tag.name if tag else None)) self.context_menu.addSeparator() self.context_menu.addAction( _('Change category icon'), partial(self.context_menu_handler, action='set_icon', key=key)) self.context_menu.addAction( _('Restore default icon'), partial(self.context_menu_handler, action='clear_icon', key=key)) # Always show the user categories editor self.context_menu.addSeparator() if key.startswith('@') and \ key[1:] in self.db.prefs.get('user_categories', {}).keys(): self.context_menu.addAction( _('Manage User Categories'), partial(self.context_menu_handler, action='manage_categories', category=key[1:])) else: self.context_menu.addAction( _('Manage User Categories'), partial(self.context_menu_handler, action='manage_categories', category=None)) if self.hidden_categories: if not self.context_menu.isEmpty(): self.context_menu.addSeparator() self.context_menu.addAction( _('Show all categories'), partial(self.context_menu_handler, action='defaults')) m = self.context_menu.addMenu(_('Change sub-categorization scheme')) da = m.addAction( _('Disable'), partial(self.context_menu_handler, action='categorization', category='disable')) fla = m.addAction( _('By first letter'), partial(self.context_menu_handler, action='categorization', category='first letter')) pa = m.addAction( _('Partition'), partial(self.context_menu_handler, action='categorization', category='partition')) if self.collapse_model == 'disable': da.setCheckable(True) da.setChecked(True) elif self.collapse_model == 'first letter': fla.setCheckable(True) fla.setChecked(True) else: pa.setCheckable(True) pa.setChecked(True) if config['sort_tags_by'] != "name": fla.setEnabled(False) m.hovered.connect(self.collapse_menu_hovered) fla.setToolTip( _('First letter is usable only when sorting by name')) # Apparently one cannot set a tooltip to empty, so use a star and # deal with it in the hover method da.setToolTip('*') pa.setToolTip('*') if not self.context_menu.isEmpty(): self.context_menu.popup(self.mapToGlobal(point)) return True def collapse_menu_hovered(self, action): tip = action.toolTip() if tip == '*': tip = '' QToolTip.showText(QCursor.pos(), tip) def dragMoveEvent(self, event): QTreeView.dragMoveEvent(self, event) self.setDropIndicatorShown(False) index = self.indexAt(event.pos()) if not index.isValid(): return src_is_tb = event.mimeData().hasFormat( 'application/calibre+from_tag_browser') item = index.data(Qt.UserRole).toPyObject() if item.type == TagTreeItem.ROOT: return flags = self._model.flags(index) if item.type == TagTreeItem.TAG and flags & Qt.ItemIsDropEnabled: self.setDropIndicatorShown(not src_is_tb) return if item.type == TagTreeItem.CATEGORY and not item.is_gst: fm_dest = self.db.metadata_for_field(item.category_key) if fm_dest['kind'] == 'user': if src_is_tb: if event.dropAction() == Qt.MoveAction: data = str(event.mimeData().data( 'application/calibre+from_tag_browser')) src = cPickle.loads(data) for s in src: if s[0] == TagTreeItem.TAG and \ (not s[1].startswith('@') or s[2]): return self.setDropIndicatorShown(True) return md = event.mimeData() if hasattr(md, 'column_name'): fm_src = self.db.metadata_for_field(md.column_name) if md.column_name in ['authors', 'publisher', 'series'] or \ (fm_src['is_custom'] and ( (fm_src['datatype'] in ['series', 'text', 'enumeration'] and not fm_src['is_multiple']) or (fm_src['datatype'] == 'composite' and fm_src['display'].get('make_category', False)))): self.setDropIndicatorShown(True) def clear(self): if self.model(): self.model().clear_state() def is_visible(self, idx): item = idx.data(Qt.UserRole).toPyObject() if getattr(item, 'type', None) == TagTreeItem.TAG: idx = idx.parent() return self.isExpanded(idx) def recount(self, *args): ''' Rebuild the category tree, expand any categories that were expanded, reset the search states, and reselect the current node. ''' if self.disable_recounting or not self.pane_is_visible: return self.refresh_signal_processed = True ci = self.currentIndex() if not ci.isValid(): ci = self.indexAt(QPoint(10, 10)) path = self.model().path_for_index(ci) if self.is_visible(ci) else None expanded_categories, state_map = self.get_state() self._model.rebuild_node_tree(state_map=state_map) self.blockSignals(True) for category in expanded_categories: idx = self._model.index_for_category(category) if idx is not None and idx.isValid(): self.expand(idx) self.show_item_at_path(path) self.blockSignals(False) def show_item_at_path(self, path, box=False, position=QTreeView.PositionAtCenter): ''' Scroll the browser and open categories to show the item referenced by path. If possible, the item is placed in the center. If box=True, a box is drawn around the item. ''' if path: self.show_item_at_index(self._model.index_for_path(path), box=box, position=position) def expand_parent(self, idx): # Needed otherwise Qt sometimes segfaults if the node is buried in a # collapsed, off screen hierarchy. To be safe, we expand from the # outermost in p = self._model.parent(idx) if p.isValid(): self.expand_parent(p) self.expand(idx) def show_item_at_index(self, idx, box=False, position=QTreeView.PositionAtCenter): if idx.isValid() and idx.data( Qt.UserRole).toPyObject() is not self._model.root_item: self.expand_parent(idx) self.setCurrentIndex(idx) self.scrollTo(idx, position) if box: self._model.set_boxed(idx) def item_expanded(self, idx): ''' Called by the expanded signal ''' self.setCurrentIndex(idx)
class Ui_DnaSequenceEditor(PM_DockWidget): """ The Ui_DnaSequenceEditor class defines UI elements for the Sequence Editor object. The sequence editor is usually visible while in while editing a DnaStrand. It is a DockWidget that is docked at the bottom of the MainWindow. """ _title = "Sequence Editor" _groupBoxCount = 0 _lastGroupBox = None def __init__(self, win): """ Constructor for the Ui_DnaSequenceEditor @param win: The parentWidget (MainWindow) for the sequence editor """ self.win = win # Should parentWidget for a docwidget always be win? #Not necessary but most likely it will be the case. parentWidget = win _superclass.__init__(self, parentWidget, title=self._title) #A flag used to restore the state of the Reports dock widget #(which can be accessed through View > Reports) see self.show() and #self.closeEvent() for more details. self._reportsDockWidget_closed_in_show_method = False self.setFixedHeight(90) return def show(self): """ Shows the sequence editor. While doing this, it also closes the reports dock widget (if visible) the state of the reports dockwidget will be restored when the sequence editor is closed. @see:self.closeEvent() """ self._reportsDockWidget_closed_in_show_method = False #hide the history widget first #(It will be shown back during self.close) #The history widget is hidden or shown only when both # 'View > Full Screen' and View > Semi Full Screen actions # are *unchecked* #Thus show or close methods won't do anything to history widget # if either of the above mentioned actions is checked. if self.win.viewFullScreenAction.isChecked() or \ self.win.viewSemiFullScreenAction.isChecked(): pass else: if self.win.reportsDockWidget.isVisible(): self.win.reportsDockWidget.close() self._reportsDockWidget_closed_in_show_method = True _superclass.show(self) return def closeEvent(self, event): """ Overrides close event. Makes sure that the visible state of the reports widgetis restored when the sequence editor is closed. @see: self.show() """ _superclass.closeEvent(self, event) if self.win.viewFullScreenAction.isChecked() or \ self.win.viewSemiFullScreenAction.isChecked(): pass else: if self._reportsDockWidget_closed_in_show_method: self.win.viewReportsAction.setChecked(True) self._reportsDockWidget_closed_in_show_method = False return def _loadWidgets(self): """ Overrides PM.PM_DockWidget._loadWidgets. Loads the widget in this dockwidget. """ self._loadMenuWidgets() self._loadTextEditWidget() return def _loadMenuWidgets(self): """ Load the various menu widgets (e.g. Open, save sequence options, Find and replace widgets etc. """ #Note: Find and replace widgets might be moved to their own class. self.loadSequenceButton = PM_ToolButton( self, iconPath="ui/actions/Properties Manager/Open.png") self.saveSequenceButton = PM_ToolButton( self, iconPath="ui/actions/Properties Manager/Save_Strand_Sequence.png") self.loadSequenceButton.setAutoRaise(True) self.saveSequenceButton.setAutoRaise(True) # Only supporting 5' to 3' direction until bug 2956 is fixed. # Mark 2008-12-19 editDirectionChoices = ["5' to 3'"] # , "3' to 5'"] self.baseDirectionChoiceComboBox = \ PM_ComboBox( self, choices = editDirectionChoices, index = 0, spanWidth = False ) #Find and replace widgets -- self.findLineEdit = \ PM_LineEdit( self, label = "", spanWidth = False) self.findLineEdit.setMaximumWidth(60) self.replaceLineEdit = \ PM_LineEdit( self, label = "", spanWidth = False) self.replaceLineEdit.setMaximumWidth(60) self.findOptionsToolButton = PM_ToolButton(self) self.findOptionsToolButton.setMaximumWidth(12) self.findOptionsToolButton.setAutoRaise(True) self.findOptionsToolButton.setPopupMode(QToolButton.MenuButtonPopup) self._setFindOptionsToolButtonMenu() self.findNextToolButton = PM_ToolButton( self, iconPath="ui/actions/Properties Manager/Find_Next.png") self.findNextToolButton.setAutoRaise(True) self.findPreviousToolButton = PM_ToolButton( self, iconPath="ui/actions/Properties Manager/Find_Previous.png") self.findPreviousToolButton.setAutoRaise(True) self.replacePushButton = PM_PushButton(self, text="Replace") self.warningSign = QLabel(self) self.warningSign.setPixmap( getpixmap('ui/actions/Properties Manager/Warning.png')) self.warningSign.hide() self.phraseNotFoundLabel = QLabel(self) self.phraseNotFoundLabel.setText("Sequence Not Found") self.phraseNotFoundLabel.hide() # NOTE: Following needs cleanup in the PM_WidgetRow/ PM_WidgetGrid # but this explanation is sufficient until thats done -- # When the widget type starts with the word 'PM_' , the # PM_WidgetRow treats it as a well defined widget and thus doesn't try # to create a QWidget object (or its subclasses) # This is the reason why qLabels such as self.warningSign and # self.phraseNotFoundLabel are defined as PM_Labels and not 'QLabels' # If they were defined as 'QLabel'(s) then PM_WidgetRow would have # recreated the label. Since we want to show/hide the above mentioned # labels (and if they were recreated as mentioned above), # we would have needed to define those something like this: # self.phraseNotFoundLabel = widgetRow._widgetList[-2] #Cleanup in PM_widgetGrid could be to check if the widget starts with #'Q' instead of 'PM_' #Widgets to include in the widget row. widgetList = [('PM_ToolButton', self.loadSequenceButton, 0), ('PM_ToolButton', self.saveSequenceButton, 1), ('QLabel', " Sequence direction:", 2), ('PM_ComboBox', self.baseDirectionChoiceComboBox, 3), ('QLabel', " Find:", 4), ('PM_LineEdit', self.findLineEdit, 5), ('PM_ToolButton', self.findOptionsToolButton, 6), ('PM_ToolButton', self.findPreviousToolButton, 7), ('PM_ToolButton', self.findNextToolButton, 8), ('QLabel', " Replace:", 9), ('PM_TextEdit', self.replaceLineEdit, 10), ('PM_PushButton', self.replacePushButton, 11), ('PM_Label', self.warningSign, 12), ('PM_Label', self.phraseNotFoundLabel, 13), ('QSpacerItem', 5, 5, 14)] widgetRow = PM_WidgetRow(self, title='', widgetList=widgetList, label="", spanWidth=True) return def _loadTextEditWidget(self): """ Load the SequenceTexteditWidgets. """ self.sequenceTextEdit = \ PM_TextEdit( self, label = " Sequence: ", spanWidth = False, permit_enter_keystroke = False) self.sequenceTextEdit.setCursorWidth(2) self.sequenceTextEdit.setWordWrapMode(QTextOption.WrapAnywhere) self.sequenceTextEdit.setFixedHeight(20) #The StrandSequence 'Mate' it is a read only etxtedit that shows #the complementary strand sequence. self.sequenceTextEdit_mate = \ PM_TextEdit(self, label = "", spanWidth = False, permit_enter_keystroke = False ) palette = getPalette(None, QPalette.Base, sequenceEditStrandMateBaseColor) self.sequenceTextEdit_mate.setPalette(palette) self.sequenceTextEdit_mate.setFixedHeight(20) self.sequenceTextEdit_mate.setReadOnly(True) self.sequenceTextEdit_mate.setWordWrapMode(QTextOption.WrapAnywhere) #Important to make sure that the horizontal and vertical scrollbars #for these text edits are never displayed. for textEdit in (self.sequenceTextEdit, self.sequenceTextEdit_mate): textEdit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) textEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) return def _getFindLineEditStyleSheet(self): """ Return the style sheet for the findLineEdit. This sets the following properties only: - background-color This style is set whenever the searchStrig can't be found (sets a light red color background to the lineedit when this happens) @return: The line edit style sheet. @rtype: str """ styleSheet = "QLineEdit {"\ "background-color: rgb(255, 102, 102)"\ "}" #Not used: # background-color: rgb(217, 255, 216)\ return styleSheet def _setFindOptionsToolButtonMenu(self): """ Sets the menu for the findOptionstoolbutton that appears a small menu button next to the findLineEdit. """ self.findOptionsMenu = QMenu(self.findOptionsToolButton) self.caseSensitiveFindAction = QAction(self.findOptionsToolButton) self.caseSensitiveFindAction.setText('Match Case') self.caseSensitiveFindAction.setCheckable(True) self.caseSensitiveFindAction.setChecked(False) self.findOptionsMenu.addAction(self.caseSensitiveFindAction) self.findOptionsMenu.addSeparator() self.findOptionsToolButton.setMenu(self.findOptionsMenu) return def _addToolTipText(self): """ What's Tool Tip text for widgets in this Property Manager. """ from ne1_ui.ToolTipText_for_PropertyManagers import ToolTip_DnaSequenceEditor ToolTip_DnaSequenceEditor(self) return def _addWhatsThisText(self): """ What's This text for widgets in this Property Manager. """ from ne1_ui.WhatsThisText_for_PropertyManagers import whatsThis_DnaSequenceEditor whatsThis_DnaSequenceEditor(self) return
def show_context_menu(self, point): item = self.itemAt(point) if item is None or item in set(self.categories.itervalues()): return m = QMenu(self) sel = self.selectedItems() num = len(sel) container = current_container() ci = self.currentItem() if ci is not None: cn = unicode(ci.data(0, NAME_ROLE).toString()) mt = unicode(ci.data(0, MIME_ROLE).toString()) cat = unicode(ci.data(0, CATEGORY_ROLE).toString()) n = elided_text(cn.rpartition('/')[-1]) m.addAction(QIcon(I('save.png')), _('Export %s') % n, partial(self.export, cn)) if cn not in container.names_that_must_not_be_changed and cn not in container.names_that_must_not_be_removed and mt not in OEB_FONTS: m.addAction( _('Replace %s with file...') % n, partial(self.replace, cn)) m.addSeparator() m.addAction(QIcon(I('modified.png')), _('&Rename %s') % n, self.edit_current_item) if is_raster_image(mt): m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover image') % n, partial(self.mark_as_cover, cn)) elif current_container( ).SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text': m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover page') % n, partial(self.mark_as_titlepage, cn)) m.addSeparator() if num > 0: m.addSeparator() if num > 1: m.addAction(QIcon(I('modified.png')), _('&Bulk rename selected files'), self.request_bulk_rename) m.addAction(QIcon(I('trash.png')), _('&Delete the %d selected file(s)') % num, self.request_delete) m.addSeparator() selected_map = defaultdict(list) for item in sel: selected_map[unicode(item.data( 0, CATEGORY_ROLE).toString())].append( unicode(item.data(0, NAME_ROLE).toString())) for items in selected_map.itervalues(): items.sort(key=self.index_of_name) if selected_map['text']: m.addAction(QIcon(I('format-text-color.png')), _('Link &stylesheets...'), partial(self.link_stylesheets, selected_map['text'])) if len(selected_map['text']) > 1: m.addAction( QIcon(I('merge.png')), _('&Merge selected text files'), partial(self.start_merge, 'text', selected_map['text'])) if len(selected_map['styles']) > 1: m.addAction( QIcon(I('merge.png')), _('&Merge selected style files'), partial(self.start_merge, 'styles', selected_map['styles'])) if len(list(m.actions())) > 0: m.popup(self.mapToGlobal(point))
class BooksView(QTableView): # {{{ files_dropped = pyqtSignal(object) add_column_signal = pyqtSignal() def viewportEvent(self, event): if (event.type() == event.ToolTip and not gprefs['book_list_tooltips']): return False return QTableView.viewportEvent(self, event) def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True): QTableView.__init__(self, parent) if not tweaks['horizontal_scrolling_per_column']: self.setHorizontalScrollMode(self.ScrollPerPixel) self.setEditTriggers(self.EditKeyPressed) if tweaks['doubleclick_on_library_view'] == 'edit_cell': self.setEditTriggers(self.DoubleClicked|self.editTriggers()) elif tweaks['doubleclick_on_library_view'] == 'open_viewer': self.setEditTriggers(self.SelectedClicked|self.editTriggers()) self.doubleClicked.connect(parent.iactions['View'].view_triggered) elif tweaks['doubleclick_on_library_view'] == 'edit_metadata': # Must not enable single-click to edit, or the field will remain # open in edit mode underneath the edit metadata dialog if use_edit_metadata_dialog: self.doubleClicked.connect( partial(parent.iactions['Edit Metadata'].edit_metadata, checked=False)) else: self.setEditTriggers(self.DoubleClicked|self.editTriggers()) self.drag_allowed = True self.setDragEnabled(True) self.setDragDropOverwriteMode(False) self.setDragDropMode(self.DragDrop) self.drag_start_pos = None self.setAlternatingRowColors(True) self.setSelectionBehavior(self.SelectRows) self.setShowGrid(False) self.setWordWrap(False) self.rating_delegate = RatingDelegate(self) self.timestamp_delegate = DateDelegate(self) self.pubdate_delegate = PubDateDelegate(self) self.last_modified_delegate = DateDelegate(self, tweak_name='gui_last_modified_display_format') self.languages_delegate = LanguagesDelegate(self) self.tags_delegate = CompleteDelegate(self, ',', 'all_tag_names') self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True) self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True) self.series_delegate = TextDelegate(self) self.publisher_delegate = TextDelegate(self) self.text_delegate = TextDelegate(self) self.cc_text_delegate = CcTextDelegate(self) self.cc_enum_delegate = CcEnumDelegate(self) self.cc_bool_delegate = CcBoolDelegate(self) self.cc_comments_delegate = CcCommentsDelegate(self) self.cc_template_delegate = CcTemplateDelegate(self) self.cc_number_delegate = CcNumberDelegate(self) self.display_parent = parent self._model = modelcls(self) self.setModel(self._model) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSortingEnabled(True) self.selectionModel().currentRowChanged.connect(self._model.current_changed) self.preserve_state = partial(PreserveViewState, self) # {{{ Column Header setup self.can_add_columns = True self.was_restored = False self.column_header = self.horizontalHeader() self.column_header.setMovable(True) self.column_header.sectionMoved.connect(self.save_state) self.column_header.setContextMenuPolicy(Qt.CustomContextMenu) self.column_header.customContextMenuRequested.connect(self.show_column_header_context_menu) self.column_header.sectionResized.connect(self.column_resized, Qt.QueuedConnection) # }}} self._model.database_changed.connect(self.database_changed) hv = self.verticalHeader() hv.setClickable(True) hv.setCursor(Qt.PointingHandCursor) self.selected_ids = [] self._model.about_to_be_sorted.connect(self.about_to_be_sorted) self._model.sorting_done.connect(self.sorting_done, type=Qt.QueuedConnection) # Column Header Context Menu {{{ def column_header_context_handler(self, action=None, column=None): if not action or not column: return try: idx = self.column_map.index(column) except: return h = self.column_header if action == 'hide': h.setSectionHidden(idx, True) elif action == 'show': h.setSectionHidden(idx, False) if h.sectionSize(idx) < 3: sz = h.sectionSizeHint(idx) h.resizeSection(idx, sz) elif action == 'ascending': self.sortByColumn(idx, Qt.AscendingOrder) elif action == 'descending': self.sortByColumn(idx, Qt.DescendingOrder) elif action == 'defaults': self.apply_state(self.get_default_state()) elif action == 'addcustcol': self.add_column_signal.emit() elif action.startswith('align_'): alignment = action.partition('_')[-1] self._model.change_alignment(column, alignment) self.save_state() def show_column_header_context_menu(self, pos): idx = self.column_header.logicalIndexAt(pos) if idx > -1 and idx < len(self.column_map): col = self.column_map[idx] name = unicode(self.model().headerData(idx, Qt.Horizontal, Qt.DisplayRole).toString()) self.column_header_context_menu = QMenu(self) if col != 'ondevice': self.column_header_context_menu.addAction(_('Hide column %s') % name, partial(self.column_header_context_handler, action='hide', column=col)) m = self.column_header_context_menu.addMenu( _('Sort on %s') % name) a = m.addAction(_('Ascending'), partial(self.column_header_context_handler, action='ascending', column=col)) d = m.addAction(_('Descending'), partial(self.column_header_context_handler, action='descending', column=col)) if self._model.sorted_on[0] == col: ac = a if self._model.sorted_on[1] else d ac.setCheckable(True) ac.setChecked(True) if col not in ('ondevice', 'inlibrary') and \ (not self.model().is_custom_column(col) or \ self.model().custom_columns[col]['datatype'] not in ('bool', )): m = self.column_header_context_menu.addMenu( _('Change text alignment for %s') % name) al = self._model.alignment_map.get(col, 'left') for x, t in (('left', _('Left')), ('right', _('Right')), ('center', _('Center'))): a = m.addAction(t, partial(self.column_header_context_handler, action='align_'+x, column=col)) if al == x: a.setCheckable(True) a.setChecked(True) hidden_cols = [self.column_map[i] for i in range(self.column_header.count()) if self.column_header.isSectionHidden(i)] try: hidden_cols.remove('ondevice') except: pass if hidden_cols: self.column_header_context_menu.addSeparator() m = self.column_header_context_menu.addMenu(_('Show column')) for col in hidden_cols: hidx = self.column_map.index(col) name = unicode(self.model().headerData(hidx, Qt.Horizontal, Qt.DisplayRole).toString()) m.addAction(name, partial(self.column_header_context_handler, action='show', column=col)) self.column_header_context_menu.addSeparator() self.column_header_context_menu.addAction( _('Shrink column if it is too wide to fit'), partial(self.resize_column_to_fit, column=self.column_map[idx])) self.column_header_context_menu.addAction( _('Restore default layout'), partial(self.column_header_context_handler, action='defaults', column=col)) if self.can_add_columns: self.column_header_context_menu.addAction( QIcon(I('column.png')), _('Add your own columns'), partial(self.column_header_context_handler, action='addcustcol', column=col)) self.column_header_context_menu.popup(self.column_header.mapToGlobal(pos)) # }}} # Sorting {{{ def about_to_be_sorted(self, idc): selected_rows = [r.row() for r in self.selectionModel().selectedRows()] self.selected_ids = [idc(r) for r in selected_rows] def sorting_done(self, indexc): pos = self.horizontalScrollBar().value() self.select_rows(self.selected_ids, using_ids=True, change_current=True, scroll=True) self.selected_ids = [] self.horizontalScrollBar().setValue(pos) def sort_by_named_field(self, field, order, reset=True): if field in self.column_map: idx = self.column_map.index(field) if order: self.sortByColumn(idx, Qt.AscendingOrder) else: self.sortByColumn(idx, Qt.DescendingOrder) else: self._model.sort_by_named_field(field, order, reset) def multisort(self, fields, reset=True, only_if_different=False): if len(fields) == 0: return sh = self.cleanup_sort_history(self._model.sort_history, ignore_column_map=True) if only_if_different and len(sh) >= len(fields): ret=True for i,t in enumerate(fields): if t[0] != sh[i][0]: ret = False break if ret: return for n,d in reversed(fields): if n in self._model.db.field_metadata.keys(): sh.insert(0, (n, d)) sh = self.cleanup_sort_history(sh, ignore_column_map=True) self._model.sort_history = [tuple(x) for x in sh] self._model.resort(reset=reset) col = fields[0][0] dir = Qt.AscendingOrder if fields[0][1] else Qt.DescendingOrder if col in self.column_map: col = self.column_map.index(col) hdrs = self.horizontalHeader() try: hdrs.setSortIndicator(col, dir) except: pass # }}} # Ondevice column {{{ def set_ondevice_column_visibility(self): m = self._model self.column_header.setSectionHidden(m.column_map.index('ondevice'), not m.device_connected) def set_device_connected(self, is_connected): self._model.set_device_connected(is_connected) self.set_ondevice_column_visibility() # }}} # Save/Restore State {{{ def get_state(self): h = self.column_header cm = self.column_map state = {} state['hidden_columns'] = [cm[i] for i in range(h.count()) if h.isSectionHidden(i) and cm[i] != 'ondevice'] state['last_modified_injected'] = True state['languages_injected'] = True state['sort_history'] = \ self.cleanup_sort_history(self.model().sort_history) state['column_positions'] = {} state['column_sizes'] = {} state['column_alignment'] = self._model.alignment_map for i in range(h.count()): name = cm[i] state['column_positions'][name] = h.visualIndex(i) if name != 'ondevice': state['column_sizes'][name] = h.sectionSize(i) return state def write_state(self, state): db = getattr(self.model(), 'db', None) name = unicode(self.objectName()) if name and db is not None: db.prefs.set(name + ' books view state', state) def save_state(self): # Only save if we have been initialized (set_database called) if len(self.column_map) > 0 and self.was_restored: state = self.get_state() self.write_state(state) def cleanup_sort_history(self, sort_history, ignore_column_map=False): history = [] for col, order in sort_history: if not isinstance(order, bool): continue if col == 'date': col = 'timestamp' if ignore_column_map or col in self.column_map: if (not history or history[-1][0] != col): history.append([col, order]) return history def apply_sort_history(self, saved_history, max_sort_levels=3): if not saved_history: return for col, order in reversed(self.cleanup_sort_history( saved_history)[:max_sort_levels]): self.sortByColumn(self.column_map.index(col), Qt.AscendingOrder if order else Qt.DescendingOrder) def apply_state(self, state, max_sort_levels=3): h = self.column_header cmap = {} hidden = state.get('hidden_columns', []) for i, c in enumerate(self.column_map): cmap[c] = i if c != 'ondevice': h.setSectionHidden(i, c in hidden) positions = state.get('column_positions', {}) pmap = {} for col, pos in positions.items(): if col in cmap: pmap[pos] = col for pos in sorted(pmap.keys()): col = pmap[pos] idx = cmap[col] current_pos = h.visualIndex(idx) if current_pos != pos: h.moveSection(current_pos, pos) sizes = state.get('column_sizes', {}) for col, size in sizes.items(): if col in cmap: sz = sizes[col] if sz < 3: sz = h.sectionSizeHint(cmap[col]) h.resizeSection(cmap[col], sz) self.apply_sort_history(state.get('sort_history', None), max_sort_levels=max_sort_levels) for col, alignment in state.get('column_alignment', {}).items(): self._model.change_alignment(col, alignment) for i in range(h.count()): if not h.isSectionHidden(i) and h.sectionSize(i) < 3: sz = h.sectionSizeHint(i) h.resizeSection(i, sz) def get_default_state(self): old_state = { 'hidden_columns': ['last_modified', 'languages'], 'sort_history':[DEFAULT_SORT], 'column_positions': {}, 'column_sizes': {}, 'column_alignment': { 'size':'center', 'timestamp':'center', 'pubdate':'center'}, 'last_modified_injected': True, 'languages_injected': True, } h = self.column_header cm = self.column_map for i in range(h.count()): name = cm[i] old_state['column_positions'][name] = i if name != 'ondevice': old_state['column_sizes'][name] = \ min(350, max(self.sizeHintForColumn(i), h.sectionSizeHint(i))) if name in ('timestamp', 'last_modified'): old_state['column_sizes'][name] += 12 return old_state def get_old_state(self): ans = None name = unicode(self.objectName()) if name: name += ' books view state' db = getattr(self.model(), 'db', None) if db is not None: ans = db.prefs.get(name, None) if ans is None: ans = gprefs.get(name, None) try: del gprefs[name] except: pass if ans is not None: db.prefs[name] = ans else: injected = False if not ans.get('last_modified_injected', False): injected = True ans['last_modified_injected'] = True hc = ans.get('hidden_columns', []) if 'last_modified' not in hc: hc.append('last_modified') if not ans.get('languages_injected', False): injected = True ans['languages_injected'] = True hc = ans.get('hidden_columns', []) if 'languages' not in hc: hc.append('languages') if injected: db.prefs[name] = ans return ans def restore_state(self): old_state = self.get_old_state() if old_state is None: old_state = self.get_default_state() max_levels = 3 if tweaks['sort_columns_at_startup'] is not None: sh = [] try: for c,d in tweaks['sort_columns_at_startup']: if not isinstance(d, bool): d = True if d == 0 else False sh.append((c, d)) except: # Ignore invalid tweak values as users seem to often get them # wrong import traceback traceback.print_exc() old_state['sort_history'] = sh max_levels = max(3, len(sh)) self.column_header.blockSignals(True) self.apply_state(old_state, max_sort_levels=max_levels) self.column_header.blockSignals(False) # Resize all rows to have the correct height if self.model().rowCount(QModelIndex()) > 0: self.resizeRowToContents(0) self.verticalHeader().setDefaultSectionSize(self.rowHeight(0)) self.was_restored = True def resize_column_to_fit(self, column): col = self.column_map.index(column) self.column_resized(col, self.columnWidth(col), self.columnWidth(col)) def column_resized(self, col, old_size, new_size): # arbitrary: scroll bar + header + some max_width = self.width() - (self.verticalScrollBar().width() + self.verticalHeader().width() + 10) if max_width < 200: max_width = 200 if new_size > max_width: self.column_header.blockSignals(True) self.setColumnWidth(col, max_width) self.column_header.blockSignals(False) # }}} # Initialization/Delegate Setup {{{ def set_database(self, db): self.save_state() self._model.set_database(db) self.tags_delegate.set_database(db) self.cc_names_delegate.set_database(db) self.authors_delegate.set_database(db) self.series_delegate.set_auto_complete_function(db.all_series) self.publisher_delegate.set_auto_complete_function(db.all_publishers) def database_changed(self, db): for i in range(self.model().columnCount(None)): if self.itemDelegateForColumn(i) in (self.rating_delegate, self.timestamp_delegate, self.pubdate_delegate, self.last_modified_delegate, self.languages_delegate): self.setItemDelegateForColumn(i, self.itemDelegate()) cm = self.column_map for colhead in cm: if self._model.is_custom_column(colhead): cc = self._model.custom_columns[colhead] if cc['datatype'] == 'datetime': delegate = CcDateDelegate(self) delegate.set_format(cc['display'].get('date_format','')) self.setItemDelegateForColumn(cm.index(colhead), delegate) elif cc['datatype'] == 'comments': self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate) elif cc['datatype'] == 'text': if cc['is_multiple']: if cc['display'].get('is_names', False): self.setItemDelegateForColumn(cm.index(colhead), self.cc_names_delegate) else: self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate) else: self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate) elif cc['datatype'] == 'series': self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate) elif cc['datatype'] in ('int', 'float'): self.setItemDelegateForColumn(cm.index(colhead), self.cc_number_delegate) elif cc['datatype'] == 'bool': self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate) elif cc['datatype'] == 'rating': self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate) elif cc['datatype'] == 'composite': self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate) elif cc['datatype'] == 'enumeration': self.setItemDelegateForColumn(cm.index(colhead), self.cc_enum_delegate) else: dattr = colhead+'_delegate' delegate = colhead if hasattr(self, dattr) else 'text' self.setItemDelegateForColumn(cm.index(colhead), getattr(self, delegate+'_delegate')) self.restore_state() self.set_ondevice_column_visibility() #}}} # Context Menu {{{ def set_context_menu(self, menu, edit_collections_action): self.setContextMenuPolicy(Qt.DefaultContextMenu) self.context_menu = menu self.edit_collections_action = edit_collections_action def contextMenuEvent(self, event): self.context_menu.popup(event.globalPos()) event.accept() # }}} # Drag 'n Drop {{{ @classmethod def paths_from_event(cls, event): ''' Accept a drop event and return a list of paths that can be read from and represent files with extensions. ''' md = event.mimeData() if md.hasFormat('text/uri-list') and not \ md.hasFormat('application/calibre+from_library'): urls = [unicode(u.toLocalFile()) for u in md.urls()] return [u for u in urls if os.path.splitext(u)[1] and os.path.exists(u)] def drag_icon(self, cover, multiple): cover = cover.scaledToHeight(120, Qt.SmoothTransformation) if multiple: base_width = cover.width() base_height = cover.height() base = QImage(base_width+21, base_height+21, QImage.Format_ARGB32_Premultiplied) base.fill(QColor(255, 255, 255, 0).rgba()) p = QPainter(base) rect = QRect(20, 0, base_width, base_height) p.fillRect(rect, QColor('white')) p.drawRect(rect) rect.moveLeft(10) rect.moveTop(10) p.fillRect(rect, QColor('white')) p.drawRect(rect) rect.moveLeft(0) rect.moveTop(20) p.fillRect(rect, QColor('white')) p.save() p.setCompositionMode(p.CompositionMode_SourceAtop) p.drawImage(rect.topLeft(), cover) p.restore() p.drawRect(rect) p.end() cover = base return QPixmap.fromImage(cover) def drag_data(self): m = self.model() db = m.db rows = self.selectionModel().selectedRows() selected = list(map(m.id, rows)) ids = ' '.join(map(str, selected)) md = QMimeData() md.setData('application/calibre+from_library', ids) fmt = prefs['output_format'] def url_for_id(i): try: ans = db.format_path(i, fmt, index_is_id=True) except: ans = None if ans is None: fmts = db.formats(i, index_is_id=True) if fmts: fmts = fmts.split(',') else: fmts = [] for f in fmts: try: ans = db.format_path(i, f, index_is_id=True) except: ans = None if ans is None: ans = db.abspath(i, index_is_id=True) return QUrl.fromLocalFile(ans) md.setUrls([url_for_id(i) for i in selected]) drag = QDrag(self) col = self.selectionModel().currentIndex().column() md.column_name = self.column_map[col] drag.setMimeData(md) cover = self.drag_icon(m.cover(self.currentIndex().row()), len(selected) > 1) drag.setHotSpot(QPoint(-15, -15)) drag.setPixmap(cover) return drag def event_has_mods(self, event=None): mods = event.modifiers() if event is not None else \ QApplication.keyboardModifiers() return mods & Qt.ControlModifier or mods & Qt.ShiftModifier def mousePressEvent(self, event): ep = event.pos() if self.indexAt(ep) in self.selectionModel().selectedIndexes() and \ event.button() == Qt.LeftButton and not self.event_has_mods(): self.drag_start_pos = ep return QTableView.mousePressEvent(self, event) def mouseMoveEvent(self, event): if not self.drag_allowed: return if self.drag_start_pos is None: return QTableView.mouseMoveEvent(self, event) if self.event_has_mods(): self.drag_start_pos = None return if not (event.buttons() & Qt.LeftButton) or \ (event.pos() - self.drag_start_pos).manhattanLength() \ < QApplication.startDragDistance(): return index = self.indexAt(event.pos()) if not index.isValid(): return drag = self.drag_data() drag.exec_(Qt.CopyAction) self.drag_start_pos = None def dragEnterEvent(self, event): if int(event.possibleActions() & Qt.CopyAction) + \ int(event.possibleActions() & Qt.MoveAction) == 0: return paths = self.paths_from_event(event) if paths: event.acceptProposedAction() def dragMoveEvent(self, event): event.acceptProposedAction() def dropEvent(self, event): paths = self.paths_from_event(event) event.setDropAction(Qt.CopyAction) event.accept() self.files_dropped.emit(paths) # }}} @property def column_map(self): return self._model.column_map def refresh_book_details(self): idx = self.currentIndex() if idx.isValid(): self._model.current_changed(idx, idx) def scrollContentsBy(self, dx, dy): # Needed as Qt bug causes headerview to not always update when scrolling QTableView.scrollContentsBy(self, dx, dy) if dy != 0: self.column_header.update() def scroll_to_row(self, row): if row > -1: h = self.horizontalHeader() for i in range(h.count()): if not h.isSectionHidden(i) and h.sectionViewportPosition(i) >= 0: self.scrollTo(self.model().index(row, i), self.PositionAtCenter) break def set_current_row(self, row, select=True): if row > -1 and row < self.model().rowCount(QModelIndex()): h = self.horizontalHeader() logical_indices = list(range(h.count())) logical_indices = [x for x in logical_indices if not h.isSectionHidden(x)] pairs = [(x, h.visualIndex(x)) for x in logical_indices if h.visualIndex(x) > -1] if not pairs: pairs = [(0, 0)] pairs.sort(cmp=lambda x,y:cmp(x[1], y[1])) i = pairs[0][0] index = self.model().index(row, i) self.setCurrentIndex(index) if select: sm = self.selectionModel() sm.select(index, sm.ClearAndSelect|sm.Rows) def select_rows(self, identifiers, using_ids=True, change_current=True, scroll=True): ''' Select rows identified by identifiers. identifiers can be a set of ids, row numbers or QModelIndexes. ''' rows = set([x.row() if hasattr(x, 'row') else x for x in identifiers]) if using_ids: rows = set([]) identifiers = set(identifiers) m = self.model() for row in xrange(m.rowCount(QModelIndex())): if m.id(row) in identifiers: rows.add(row) rows = list(sorted(rows)) if rows: row = rows[0] if change_current: self.set_current_row(row, select=False) if scroll: self.scroll_to_row(row) sm = self.selectionModel() sel = QItemSelection() m = self.model() max_col = m.columnCount(QModelIndex()) - 1 # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda (i,x):i-x): group = list(map(operator.itemgetter(1), g)) sel.merge(QItemSelection(m.index(min(group), 0), m.index(max(group), max_col)), sm.Select) sm.select(sel, sm.ClearAndSelect)
class Ui_ProteinSequenceEditor(PM_DockWidget): """ The Ui_DnaSequenceEditor class defines UI elements for the Sequence Editor object. The sequence editor is usually visible while in DNA edit mode. It is a DockWidget that is doced at the bottom of the MainWindow """ _title = "Sequence Editor" _groupBoxCount = 0 _lastGroupBox = None def __init__(self, win): """ Constructor for the Ui_DnaSequenceEditor @param win: The parentWidget (MainWindow) for the sequence editor """ self.win = win # Should parentWidget for a docwidget always be win? #Not necessary but most likely it will be the case. parentWidget = win _superclass.__init__(self, parentWidget, title = self._title) #A flag used to restore the state of the Reports dock widget #(which can be accessed through View > Reports) see self.show() and #self.closeEvent() for more details. self._reportsDockWidget_closed_in_show_method = False self.setFixedHeight(90) def show(self): """ Shows the sequence editor. While doing this, it also closes the reports dock widget (if visible) the state of the reports dockwidget will be restored when the sequence editor is closed. @see:self.closeEvent() """ self._reportsDockWidget_closed_in_show_method = False if self.win.viewFullScreenAction.isChecked() or \ self.win.viewSemiFullScreenAction.isChecked(): pass else: if self.win.reportsDockWidget.isVisible(): self.win.reportsDockWidget.close() self._reportsDockWidget_closed_in_show_method = True _superclass.show(self) def closeEvent(self, event): """ Overrides close event. Makes sure that the visible state of the reports widgetis restored when the sequence editor is closed. @see: self.show() """ _superclass.closeEvent(self, event) if self.win.viewFullScreenAction.isChecked() or \ self.win.viewSemiFullScreenAction.isChecked(): pass else: if self._reportsDockWidget_closed_in_show_method: self.win.viewReportsAction.setChecked(True) self._reportsDockWidget_closed_in_show_method = False def _loadWidgets(self): """ Overrides PM.PM_DockWidget._loadWidgets. Loads the widget in this dockwidget. """ self._loadMenuWidgets() self._loadTextEditWidget() def _loadMenuWidgets(self): """ Load the various menu widgets (e.g. Open, save sequence options, Find and replace widgets etc. """ #Note: Find and replace widgets might be moved to their own class. self.loadSequenceButton = PM_ToolButton( self, iconPath = "ui/actions/Properties Manager/Open.png") self.saveSequenceButton = PM_ToolButton( self, iconPath = "ui/actions/Properties Manager/Save_Strand_Sequence.png") self.loadSequenceButton.setAutoRaise(True) self.saveSequenceButton.setAutoRaise(True) #Find and replace widgets -- self.findLineEdit = \ PM_LineEdit( self, label = "", spanWidth = False) self.findLineEdit.setMaximumWidth(60) self.replaceLineEdit = \ PM_LineEdit( self, label = "", spanWidth = False) self.replaceLineEdit.setMaximumWidth(60) self.findOptionsToolButton = PM_ToolButton(self) self.findOptionsToolButton.setMaximumWidth(12) self.findOptionsToolButton.setAutoRaise(True) self.findOptionsToolButton.setPopupMode(QToolButton.MenuButtonPopup) self._setFindOptionsToolButtonMenu() self.findNextToolButton = PM_ToolButton( self, iconPath = "ui/actions/Properties Manager/Find_Next.png") self.findNextToolButton.setAutoRaise(True) self.findPreviousToolButton = PM_ToolButton( self, iconPath = "ui/actions/Properties Manager/Find_Previous.png") self.findPreviousToolButton.setAutoRaise(True) self.replacePushButton = PM_PushButton(self, text = "Replace") self.warningSign = QLabel(self) self.warningSign.setPixmap( getpixmap('ui/actions/Properties Manager/Warning.png')) self.warningSign.hide() self.phraseNotFoundLabel = QLabel(self) self.phraseNotFoundLabel.setText("Sequence Not Found") self.phraseNotFoundLabel.hide() #Widgets to include in the widget row. widgetList = [('PM_ToolButton', self.loadSequenceButton, 0), ('PM_ToolButton', self.saveSequenceButton, 1), ('QLabel', " Find:", 4), ('PM_LineEdit', self.findLineEdit, 5), ('PM_ToolButton', self.findOptionsToolButton, 6), ('PM_ToolButton', self.findPreviousToolButton, 7), ('PM_ToolButton', self.findNextToolButton, 8), ('QLabel', " Replace:", 9), ('PM_TextEdit', self.replaceLineEdit, 10), ('PM_PushButton', self.replacePushButton, 11), ('PM_Label', self.warningSign, 12), ('PM_Label', self.phraseNotFoundLabel, 13), ('QSpacerItem', 5, 5, 14) ] widgetRow = PM_WidgetRow(self, title = '', widgetList = widgetList, label = "", spanWidth = True ) def _loadTextEditWidget(self): """ Load the SequenceTexteditWidgets. """ self.aaRulerTextEdit = \ PM_TextEdit( self, label = "", spanWidth = False, permit_enter_keystroke = False) palette = getPalette(None, QPalette.Base, pmGrpBoxColor) self.aaRulerTextEdit.setPalette(palette) self.aaRulerTextEdit.setWordWrapMode( QTextOption.WrapAnywhere ) self.aaRulerTextEdit.setFixedHeight(20) self.aaRulerTextEdit.setReadOnly(True) self.sequenceTextEdit = \ PM_TextEdit( self, label = " Sequence: ", spanWidth = False, permit_enter_keystroke = False) self.sequenceTextEdit.setCursorWidth(2) self.sequenceTextEdit.setWordWrapMode( QTextOption.WrapAnywhere ) self.sequenceTextEdit.setFixedHeight(20) self.secStrucTextEdit = \ PM_TextEdit( self, label = " Secondary structure: ", spanWidth = False, permit_enter_keystroke = False) palette = getPalette(None, QPalette.Base, sequenceEditStrandMateBaseColor) self.secStrucTextEdit.setPalette(palette) self.secStrucTextEdit.setWordWrapMode( QTextOption.WrapAnywhere ) self.secStrucTextEdit.setFixedHeight(20) self.secStrucTextEdit.setReadOnly(True) #Important to make sure that the horizontal and vertical scrollbars #for these text edits are never displayed. self.sequenceTextEdit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.sequenceTextEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.secStrucTextEdit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.secStrucTextEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.aaRulerTextEdit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.aaRulerTextEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) def _getFindLineEditStyleSheet(self): """ Return the style sheet for the findLineEdit. This sets the following properties only: - background-color This style is set whenever the searchStrig can't be found (sets a light red color background to the lineedit when this happens) @return: The line edit style sheet. @rtype: str """ styleSheet = \ "QLineEdit {\ background-color: rgb(255, 102, 102)\ }" #Not used: # background-color: rgb(217, 255, 216)\ return styleSheet def _setFindOptionsToolButtonMenu(self): """ Sets the menu for the findOptionstoolbutton that appears a small menu button next to the findLineEdit. """ self.findOptionsMenu = QMenu(self.findOptionsToolButton) self.caseSensitiveFindAction = QAction(self.findOptionsToolButton) self.caseSensitiveFindAction.setText('Match Case') self.caseSensitiveFindAction.setCheckable(True) self.caseSensitiveFindAction.setChecked(False) self.findOptionsMenu.addAction(self.caseSensitiveFindAction) self.findOptionsMenu.addSeparator() self.findOptionsToolButton.setMenu(self.findOptionsMenu) def _addToolTipText(self): """ What's Tool Tip text for widgets in this Property Manager. """ pass def _addWhatsThisText(self): """ What's This text for widgets in this Property Manager. """ pass
def makemenu_helper(widget, menu_spec, menu=None): """ Make and return a reusable or one-time-use (at caller's option) popup menu whose structure is specified by menu_spec, which is a list of menu item specifiers, each of which is either None (for a separator) or a tuple of the form (menu text, callable or submenu, option1, option2, ...) with 0 or more options (described below). A submenu can be either another menu_spec list, or a QMenu object (but in the latter case the menu text is ignored -- maybe it comes from that QMenu object somehow -- not sure if this was different in Qt3). In either case it is the 2nd menu-item-tuple element, in place of the callable. Otherwise the callable must satisfy the python 'callable' predicate, and is executed if the menu item is chosen, wrapped inside another function which handles Undo checkpointing and Undo-command-name setting. The options in a menu item tuple can be zero or more (in any order, duplicates allowed) of the following: 'disabled' -- the menu item should be disabled; 'checked' -- the menu item will be checked; None -- this option is legal but ignored (but the callable must still satisfy the python predicate "callable"; constants.noop might be useful for that case). The Qt3 version also supported tuple-options consisting of one of the words 'iconset' and 'whatsThis' followed by an appropriate argument, but those have not yet been ported to Qt4 (except possibly for disabled menu items -- UNTESTED). Unrecognized options may or may not generate warnings, and are otherwise ignored. [###FIX that -- they always ought to print a warning to developers. Note that right now they do iff 'disabled' is one of the options and ATOM_DEBUG is set.] The 'widget' argument should be the Qt widget which is using this function to put up a menu. If the menu argument is provided, it should be a QMenu to which we'll add items; otherwise we create our own QMenu and add items to it. """ from utilities.debug import print_compact_traceback import types if menu is None: menu = QMenu(widget) ## menu.show() #bruce 070514 removed menu.show() to fix a cosmetic and performance bug # (on Mac, possibly on other platforms too; probably unreported) # in which the debug menu first appears in screen center, slowly grows # to full size while remaining blank, then moves to its final position # and looks normal (causing a visual glitch, and a 2-3 second delay # in being able to use it). May fix similar issues with other menus. # If this causes harm for some menus or platforms, we can adapt it. # bruce 040909-16 moved this method from basicMode to GLPane, # leaving a delegator for it in basicMode. # (bruce was not the original author, but modified it) #menu = QMenu( widget) for m in menu_spec: try: #bruce 050416 added try/except as debug code and for safety menutext = m and widget.trUtf8(m[0]) if m and isinstance(m[1], QMenu): #bruce 041010 added this case submenu = m[1] #menu.insertItem( menutext, submenu ) menu.addMenu(submenu) # how do I get menutext in there? # (similar code might work for QAction case too, not sure) elif m and isinstance( m[1], types.ListType): #bruce 041103 added this case submenu = QMenu(menutext, menu) submenu = makemenu_helper( widget, m[1], submenu) # [this used to call widget.makemenu] menu.addMenu(submenu) elif m: assert callable(m[1]), \ "%r[1] needs to be a callable" % (m,) #bruce 041103 # transform m[1] into a new callable that makes undo checkpoints and provides an undo command-name # [bruce 060324 for possible bugs in undo noticing cmenu items, and for the cmdnames] func = wrap_callable_for_undo(m[1], cmdname=m[0]) # guess about cmdname, but it might be reasonable for A7 as long as we ensure weird characters won't confuse it import foundation.changes as changes changes.keep_forever( func ) # THIS IS BAD (memory leak), but it's not a severe one, so ok for A7 [bruce 060324] # (note: the hard part about removing these when we no longer need them is knowing when to do that # if the user ends up not selecting anything from the menu. Also, some callers make these # menus for reuse multiple times, and for them we never want to deallocate func even when some # menu command gets used. We could solve both of these by making the caller pass a place to keep these # which it would deallocate someday or which would ensure only one per distinct kind of menu is kept. #e) if 'disabled' not in m[2:]: act = QAction(widget) act.setText(menutext) if 'checked' in m[2:]: act.setCheckable(True) act.setChecked(True) menu.addAction(act) widget.connect(act, SIGNAL("activated()"), func) else: # disabled case # [why is this case done differently, in this Qt4 port?? -- bruce 070522 question] insert_command_into_menu(menu, menutext, func, options=m[2:], raw_command=True) else: menu.addSeparator( ) #bruce 070522 bugfix -- before this, separators were placed lower down or dropped # so as not to come before disabled items, for unknown reasons. # (Speculation: maybe because insertSeparator was used, since addSeparator didn't work or wasn't noticed, # and since disabled item were added by an older function (also for unknown reasons)?) pass except Exception, e: if isinstance(e, SystemExit): raise print_compact_traceback( "exception in makemenu_helper ignored, for %r:\n" % (m, )) #bruce 070522 restored this (was skipped in Qt4 port) pass #e could add a fake menu item here as an error message
class LocationManager(QObject): # {{{ locations_changed = pyqtSignal() unmount_device = pyqtSignal() location_selected = pyqtSignal(object) configure_device = pyqtSignal() update_device_metadata = pyqtSignal() def __init__(self, parent=None): QObject.__init__(self, parent) self.free = [-1, -1, -1] self.count = 0 self.location_actions = QActionGroup(self) self.location_actions.setExclusive(True) self.current_location = 'library' self._mem = [] self.tooltips = {} self.all_actions = [] def ac(name, text, icon, tooltip): icon = QIcon(I(icon)) ac = self.location_actions.addAction(icon, text) setattr(self, 'location_'+name, ac) ac.setAutoRepeat(False) ac.setCheckable(True) receiver = partial(self._location_selected, name) ac.triggered.connect(receiver) self.tooltips[name] = tooltip m = QMenu(parent) self._mem.append(m) a = m.addAction(icon, tooltip) a.triggered.connect(receiver) if name != 'library': self._mem.append(a) a = m.addAction(QIcon(I('eject.png')), _('Eject this device')) a.triggered.connect(self._eject_requested) self._mem.append(a) a = m.addAction(QIcon(I('config.png')), _('Configure this device')) a.triggered.connect(self._configure_requested) self._mem.append(a) a = m.addAction(QIcon(I('sync.png')), _('Update cached metadata on device')) a.triggered.connect(lambda x : self.update_device_metadata.emit()) self._mem.append(a) else: ac.setToolTip(tooltip) ac.setMenu(m) ac.calibre_name = name self.all_actions.append(ac) return ac self.library_action = ac('library', _('Library'), 'lt.png', _('Show books in calibre library')) ac('main', _('Device'), 'reader.png', _('Show books in the main memory of the device')) ac('carda', _('Card A'), 'sd.png', _('Show books in storage card A')) ac('cardb', _('Card B'), 'sd.png', _('Show books in storage card B')) def set_switch_actions(self, quick_actions, rename_actions, delete_actions, switch_actions, choose_action): self.switch_menu = self.library_action.menu() if self.switch_menu: self.switch_menu.addSeparator() else: self.switch_menu = QMenu() self.switch_menu.addAction(choose_action) self.cs_menus = [] for t, acs in [(_('Quick switch'), quick_actions), (_('Rename library'), rename_actions), (_('Delete library'), delete_actions)]: if acs: self.cs_menus.append(QMenu(t)) for ac in acs: self.cs_menus[-1].addAction(ac) self.switch_menu.addMenu(self.cs_menus[-1]) self.switch_menu.addSeparator() for ac in switch_actions: self.switch_menu.addAction(ac) if self.switch_menu != self.library_action.menu(): self.library_action.setMenu(self.switch_menu) def _location_selected(self, location, *args): if location != self.current_location and hasattr(self, 'location_'+location): self.current_location = location self.location_selected.emit(location) getattr(self, 'location_'+location).setChecked(True) def _eject_requested(self, *args): self.unmount_device.emit() def _configure_requested(self): self.configure_device.emit() def update_devices(self, cp=(None, None), fs=[-1, -1, -1], icon=None): if icon is None: icon = I('reader.png') self.location_main.setIcon(QIcon(icon)) had_device = self.has_device if cp is None: cp = (None, None) if isinstance(cp, (str, unicode)): cp = (cp, None) if len(fs) < 3: fs = list(fs) + [0] self.free[0] = fs[0] self.free[1] = fs[1] self.free[2] = fs[2] cpa, cpb = cp self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1 self.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1 self.update_tooltips() if self.has_device != had_device: self.location_library.setChecked(True) self.locations_changed.emit() if not self.has_device: self.location_library.trigger() def update_tooltips(self): for i, loc in enumerate(('main', 'carda', 'cardb')): t = self.tooltips[loc] if self.free[i] > -1: t += u'\n\n%s '%human_readable(self.free[i]) + _('available') ac = getattr(self, 'location_'+loc) ac.setToolTip(t) ac.setWhatsThis(t) ac.setStatusTip(t) @property def has_device(self): return max(self.free) > -1 @property def available_actions(self): ans = [self.location_library] for i, loc in enumerate(('main', 'carda', 'cardb')): if self.free[i] > -1: ans.append(getattr(self, 'location_'+loc)) return ans
def show_context_menu(self, pos): m = QMenu(self) a = m.addAction c = self.editor.cursorForPosition(pos) origc = QTextCursor(c) r = origr = self.editor.syntax_range_for_cursor(c) if (r is None or not r.format.property(SPELL_PROPERTY).toBool()) and c.positionInBlock() > 0: c.setPosition(c.position() - 1) r = self.editor.syntax_range_for_cursor(c) if r is not None and r.format.property(SPELL_PROPERTY).toBool(): word = self.editor.text_for_range(c.block(), r) locale = self.editor.spellcheck_locale_for_cursor(c) orig_pos = c.position() c.setPosition(orig_pos - utf16_length(word)) found = False self.editor.setTextCursor(c) if self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False): found = True fc = self.editor.textCursor() if fc.position() < c.position(): self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False) if found: suggestions = dictionaries.suggestions(word, locale)[:7] if suggestions: for suggestion in suggestions: ac = m.addAction(suggestion, partial(self.editor.simple_replace, suggestion)) f = ac.font() f.setBold(True), ac.setFont(f) m.addSeparator() m.addAction(actions['spell-next']) m.addAction(_('Ignore this word'), partial(self._nuke_word, None, word, locale)) dics = dictionaries.active_user_dictionaries if len(dics) > 0: if len(dics) == 1: m.addAction(_('Add this word to the dictionary: {0}').format(dics[0].name), partial( self._nuke_word, dics[0].name, word, locale)) else: ac = m.addAction(_('Add this word to the dictionary')) dmenu = QMenu(m) ac.setMenu(dmenu) for dic in dics: dmenu.addAction(dic.name, partial(self._nuke_word, dic.name, word, locale)) m.addSeparator() if origr is not None and origr.format.property(LINK_PROPERTY).toBool(): href = self.editor.text_for_range(origc.block(), origr) m.addAction(_('Open %s') % href, partial(self.link_clicked.emit, href)) if origr is not None and (origr.format.property(TAG_NAME_PROPERTY).toBool() or origr.format.property(CSS_PROPERTY).toBool()): word = self.editor.text_for_range(origc.block(), origr) item_type = 'tag_name' if origr.format.property(TAG_NAME_PROPERTY).toBool() else 'css_property' url = help_url(word, item_type, self.editor.highlighter.doc_name, extra_data=current_container().opf_version) if url is not None: m.addAction(_('Show help for: %s') % word, partial(open_url, url)) for x in ('undo', 'redo'): a(actions['editor-%s' % x]) m.addSeparator() for x in ('cut', 'copy', 'paste'): a(actions['editor-' + x]) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) m.addAction(actions['mark-selected-text']) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.mapToGlobal(pos))
class Ui_DnaSequenceEditor(PM_DockWidget): """ The Ui_DnaSequenceEditor class defines UI elements for the Sequence Editor object. The sequence editor is usually visible while in DNA edit mode. It is a DockWidget that is doced at the bottom of the MainWindow """ _title = "Sequence Editor" _groupBoxCount = 0 _lastGroupBox = None def __init__(self, win): """ Constructor for the Ui_DnaSequenceEditor @param win: The parentWidget (MainWindow) for the sequence editor """ self.win = win # Should parentWidget for a docwidget always be win? #Not necessary but most likely it will be the case. parentWidget = win _superclass.__init__(self, parentWidget, title = self._title) #A flag used to restore the state of the Reports dock widget #(which can be accessed through View > Reports) see self.show() and #self.closeEvent() for more details. self._reportsDockWidget_closed_in_show_method = False self.setFixedHeight(90) def show(self): """ Shows the sequence editor. While doing this, it also closes the reports dock widget (if visible) the state of the reports dockwidget will be restored when the sequence editor is closed. @see:self.closeEvent() """ self._reportsDockWidget_closed_in_show_method = False #hide the history widget first #(It will be shown back during self.close) #The history widget is hidden or shown only when both # 'View > Full Screen' and View > Semi Full Screen actions # are *unchecked* #Thus show or close methods won't do anything to history widget # if either of the above mentioned actions is checked. if self.win.viewFullScreenAction.isChecked() or \ self.win.viewSemiFullScreenAction.isChecked(): pass else: if self.win.reportsDockWidget.isVisible(): self.win.reportsDockWidget.close() self._reportsDockWidget_closed_in_show_method = True _superclass.show(self) def closeEvent(self, event): """ Overrides close event. Makes sure that the visible state of the reports widgetis restored when the sequence editor is closed. @see: self.show() """ _superclass.closeEvent(self, event) if self.win.viewFullScreenAction.isChecked() or \ self.win.viewSemiFullScreenAction.isChecked(): pass else: if self._reportsDockWidget_closed_in_show_method: self.win.viewReportsAction.setChecked(True) self._reportsDockWidget_closed_in_show_method = False def _loadWidgets(self): """ Overrides PM.PM_DockWidget._loadWidgets. Loads the widget in this dockwidget. """ self._loadMenuWidgets() self._loadTextEditWidget() def _loadMenuWidgets(self): """ Load the various menu widgets (e.g. Open, save sequence options, Find and replace widgets etc. """ #Note: Find and replace widgets might be moved to their own class. self.loadSequenceButton = PM_ToolButton( self, iconPath = "ui/actions/Properties Manager/Open.png") self.saveSequenceButton = PM_ToolButton( self, iconPath = "ui/actions/Properties Manager/Save_Strand_Sequence.png") self.loadSequenceButton.setAutoRaise(True) self.saveSequenceButton.setAutoRaise(True) editDirectionChoices = ["5' to 3'", "3' to 5'"] self.baseDirectionChoiceComboBox = \ PM_ComboBox( self, choices = editDirectionChoices, index = 0, spanWidth = False ) #Find and replace widgets -- self.findLineEdit = \ PM_LineEdit( self, label = "", spanWidth = False) self.findLineEdit.setMaximumWidth(60) self.replaceLineEdit = \ PM_LineEdit( self, label = "", spanWidth = False) self.replaceLineEdit.setMaximumWidth(60) self.findOptionsToolButton = PM_ToolButton(self) self.findOptionsToolButton.setMaximumWidth(12) self.findOptionsToolButton.setAutoRaise(True) self.findOptionsToolButton.setPopupMode(QToolButton.MenuButtonPopup) self._setFindOptionsToolButtonMenu() self.findNextToolButton = PM_ToolButton( self, iconPath = "ui/actions/Properties Manager/Find_Next.png") self.findNextToolButton.setAutoRaise(True) self.findPreviousToolButton = PM_ToolButton( self, iconPath = "ui/actions/Properties Manager/Find_Previous.png") self.findPreviousToolButton.setAutoRaise(True) self.replacePushButton = PM_PushButton(self, text = "Replace") self.warningSign = QLabel(self) self.warningSign.setPixmap( getpixmap('ui/actions/Properties Manager/Warning.png')) self.warningSign.hide() self.phraseNotFoundLabel = QLabel(self) self.phraseNotFoundLabel.setText("Sequence Not Found") self.phraseNotFoundLabel.hide() # NOTE: Following needs cleanup in the PM_WidgetRow/ PM_WidgetGrid # but this explanation is sufficient until thats done -- # When the widget type starts with the word 'PM_' , the # PM_WidgetRow treats it as a well defined widget and thus doesn't try # to create a QWidget object (or its subclasses) # This is the reason why qLabels such as self.warningSign and # self.phraseNotFoundLabel are defined as PM_Labels and not 'QLabels' # If they were defined as 'QLabel'(s) then PM_WidgetRow would have # recreated the label. Since we want to show/hide the above mentioned # labels (and if they were recreated as mentioned above), # we would have needed to define those something like this: # self.phraseNotFoundLabel = widgetRow._widgetList[-2] #Cleanup in PM_widgetGrid could be to check if the widget starts with #'Q' instead of 'PM_' #Widgets to include in the widget row. widgetList = [('PM_ToolButton', self.loadSequenceButton, 0), ('PM_ToolButton', self.saveSequenceButton, 1), ('QLabel', " Sequence direction:", 2), ('PM_ComboBox', self.baseDirectionChoiceComboBox , 3), ('QLabel', " Find:", 4), ('PM_LineEdit', self.findLineEdit, 5), ('PM_ToolButton', self.findOptionsToolButton, 6), ('PM_ToolButton', self.findPreviousToolButton, 7), ('PM_ToolButton', self.findNextToolButton, 8), ('QLabel', " Replace:", 9), ('PM_TextEdit', self.replaceLineEdit, 10), ('PM_PushButton', self.replacePushButton, 11), ('PM_Label', self.warningSign, 12), ('PM_Label', self.phraseNotFoundLabel, 13), ('QSpacerItem', 5, 5, 14) ] widgetRow = PM_WidgetRow(self, title = '', widgetList = widgetList, label = "", spanWidth = True ) def _loadTextEditWidget(self): """ Load the SequenceTexteditWidgets. """ self.sequenceTextEdit = \ PM_TextEdit( self, label = " Sequence: ", spanWidth = False, permit_enter_keystroke = False) self.sequenceTextEdit.setCursorWidth(2) self.sequenceTextEdit.setWordWrapMode( QTextOption.WrapAnywhere ) self.sequenceTextEdit.setFixedHeight(20) #The StrandSequence 'Mate' it is a read only etxtedit that shows #the complementary strand sequence. self.sequenceTextEdit_mate = \ PM_TextEdit(self, label = "", spanWidth = False, permit_enter_keystroke = False ) palette = getPalette(None, QPalette.Base, sequenceEditStrandMateBaseColor) self.sequenceTextEdit_mate.setPalette(palette) self.sequenceTextEdit_mate.setFixedHeight(20) self.sequenceTextEdit_mate.setReadOnly(True) self.sequenceTextEdit_mate.setWordWrapMode(QTextOption.WrapAnywhere) #Important to make sure that the horizontal and vertical scrollbars #for these text edits are never displayed. for textEdit in (self.sequenceTextEdit, self.sequenceTextEdit_mate): textEdit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) textEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) def _getFindLineEditStyleSheet(self): """ Return the style sheet for the findLineEdit. This sets the following properties only: - background-color This style is set whenever the searchStrig can't be found (sets a light red color background to the lineedit when this happens) @return: The line edit style sheet. @rtype: str """ styleSheet = \ "QLineEdit {\ background-color: rgb(255, 102, 102)\ }" #Not used: # background-color: rgb(217, 255, 216)\ return styleSheet def _setFindOptionsToolButtonMenu(self): """ Sets the menu for the findOptionstoolbutton that appears a small menu button next to the findLineEdit. """ self.findOptionsMenu = QMenu(self.findOptionsToolButton) self.caseSensitiveFindAction = QAction(self.findOptionsToolButton) self.caseSensitiveFindAction.setText('Match Case') self.caseSensitiveFindAction.setCheckable(True) self.caseSensitiveFindAction.setChecked(False) self.findOptionsMenu.addAction(self.caseSensitiveFindAction) self.findOptionsMenu.addSeparator() self.findOptionsToolButton.setMenu(self.findOptionsMenu) def _addToolTipText(self): """ What's Tool Tip text for widgets in this Property Manager. """ from ne1_ui.ToolTipText_for_PropertyManagers import ToolTip_SequenceEditor ToolTip_SequenceEditor(self) def _addWhatsThisText(self): """ What's This text for widgets in this Property Manager. """ from ne1_ui.WhatsThisText_for_PropertyManagers import whatsThis_SequenceEditor whatsThis_SequenceEditor(self)
def contextMenuEvent(self, event): """ Creates two context menus: 1. no modifier -> spellchecker & clear emnu 2. ctrl modifier -> Text change & Insert symbol """ contextMenu = self.createStandardContextMenu() spellMenu = True if (QApplication.keyboardModifiers() & Qt.ControlModifier): spellMenu = False self.clearAction = QtGui.QAction(self.tr("Clear"), contextMenu) contextMenu.addSeparator() contextMenu.addAction(self.clearAction) if not len(self.toPlainText()): self.clearAction.setEnabled(False) QtCore.QObject.connect(self.clearAction, QtCore.SIGNAL("triggered()"), self.clear) if not spellMenu: textOpsMenu = QMenu(self.tr("Text change...")) removeEOLAction = QtGui.QAction(self.tr("Join lines"), textOpsMenu, ) textOpsMenu.addAction(removeEOLAction) QtCore.QObject.connect(removeEOLAction, QtCore.SIGNAL("triggered()"), self.removeEOL) textOpsMenu.addSeparator() toUppercaseAction = QtGui.QAction(self.tr("to UPPERCASE"), textOpsMenu) textOpsMenu.addAction(toUppercaseAction) QtCore.QObject.connect(toUppercaseAction, QtCore.SIGNAL("triggered()"), self.toUppercase) toLowercaseAction = QtGui.QAction(self.tr("to lowercase"), textOpsMenu) textOpsMenu.addAction(toLowercaseAction) QtCore.QObject.connect(toLowercaseAction, QtCore.SIGNAL("triggered()"), self.toLowercase) toTitleAction = QtGui.QAction(self.tr("to Title"), textOpsMenu) textOpsMenu.addAction(toTitleAction) QtCore.QObject.connect(toTitleAction, QtCore.SIGNAL("triggered()"), self.toTitlecase) toCapsAction = QtGui.QAction(self.tr("to Capitalize"), textOpsMenu) textOpsMenu.addAction(toCapsAction) QtCore.QObject.connect(toCapsAction, QtCore.SIGNAL("triggered()"), self.toCaps) contextMenu.insertSeparator(contextMenu.actions()[0]) contextMenu.insertMenu(contextMenu.actions()[0], textOpsMenu) insertSymbolMenu = QMenu(self.tr("Insert symbol...")) settings_symbols = settings.get('editor:symbols') if settings_symbols: self.symbols = settings_symbols.split('\n') for symbol in self.symbols: action = SpellAction(symbol, insertSymbolMenu) action.correct.connect( self.insertSymbol) insertSymbolMenu.addAction(action) contextMenu.insertMenu(contextMenu.actions()[0], insertSymbolMenu) if not self.textCursor().hasSelection() and spellMenu: # Select the word under the cursor for spellchecker cursor = self.textCursor() cursor.select(QTextCursor.WordUnderCursor) self.setTextCursor(cursor) text = unicode(self.textCursor().selectedText()) #TODO: put to configuration list of ignored starting/ending chars # remove u"„" from selection if text.startswith(u"„") or text.startswith(u"“"): text = text[1:] selectionEnd = cursor.selectionEnd() cursor.setPosition(cursor.position() - len(text)) cursor.setPosition(selectionEnd, QTextCursor.KeepAnchor) self.setTextCursor(cursor) # remove u"”" from selection if text.endswith(u"”") or text.startswith(u"“"): selectionEnd = cursor.selectionEnd() cursor.setPosition(cursor.position() - len(text)) cursor.setPosition(selectionEnd - 1, QTextCursor.KeepAnchor) text = text[:-1] self.setTextCursor(cursor) # Check if the selected word is misspelled and offer spelling # suggestions if it is. if self.textCursor().hasSelection(): if not self.dict.check(text): spell_menu = QMenu(self.tr("Spelling Suggestions")) addWordAcction = QAction(self.tr('Add word...'), spell_menu) QtCore.QObject.connect(addWordAcction, QtCore.SIGNAL("triggered()"), self.addWord) #addWordAcction.triggered.connect(self.addWord) spell_menu.addAction(addWordAcction) for word in self.dict.suggest(text): action = SpellAction(word, spell_menu) action.correct.connect(self.changeText) spell_menu.addAction(action) contextMenu.insertSeparator(contextMenu.actions()[1]) contextMenu.insertMenu(contextMenu.actions()[0], spell_menu) # Only add the spelling suggests to the menu if there are # suggestions. if len(spell_menu.actions()) != 1: spell_menu.insertSeparator(spell_menu.actions()[1]) contextMenu.exec_(event.globalPos()) event.accept()
def contextMenuEvent(self, event): """ Creates two context menus: 1. no modifier -> spellchecker & clear emnu 2. ctrl modifier -> Text change & Insert symbol """ contextMenu = self.createStandardContextMenu() spellMenu = True if (QApplication.keyboardModifiers() & Qt.ControlModifier): spellMenu = False self.clearAction = QtGui.QAction(self.tr("Clear"), contextMenu) contextMenu.addSeparator() contextMenu.addAction(self.clearAction) if not len(self.toPlainText()): self.clearAction.setEnabled(False) QtCore.QObject.connect(self.clearAction, QtCore.SIGNAL("triggered()"), self.clear) if not spellMenu: textOpsMenu = QMenu(self.tr("Text change...")) removeEOLAction = QtGui.QAction( self.tr("Join lines"), textOpsMenu, ) textOpsMenu.addAction(removeEOLAction) QtCore.QObject.connect(removeEOLAction, QtCore.SIGNAL("triggered()"), self.removeEOL) textOpsMenu.addSeparator() toUppercaseAction = QtGui.QAction(self.tr("to UPPERCASE"), textOpsMenu) textOpsMenu.addAction(toUppercaseAction) QtCore.QObject.connect(toUppercaseAction, QtCore.SIGNAL("triggered()"), self.toUppercase) toLowercaseAction = QtGui.QAction(self.tr("to lowercase"), textOpsMenu) textOpsMenu.addAction(toLowercaseAction) QtCore.QObject.connect(toLowercaseAction, QtCore.SIGNAL("triggered()"), self.toLowercase) toTitleAction = QtGui.QAction(self.tr("to Title"), textOpsMenu) textOpsMenu.addAction(toTitleAction) QtCore.QObject.connect(toTitleAction, QtCore.SIGNAL("triggered()"), self.toTitlecase) toCapsAction = QtGui.QAction(self.tr("to Capitalize"), textOpsMenu) textOpsMenu.addAction(toCapsAction) QtCore.QObject.connect(toCapsAction, QtCore.SIGNAL("triggered()"), self.toCaps) contextMenu.insertSeparator(contextMenu.actions()[0]) contextMenu.insertMenu(contextMenu.actions()[0], textOpsMenu) insertSymbolMenu = QMenu(self.tr("Insert symbol...")) settings_symbols = settings.get('editor:symbols') if settings_symbols: self.symbols = settings_symbols.split('\n') for symbol in self.symbols: action = SpellAction(symbol, insertSymbolMenu) action.correct.connect(self.insertSymbol) insertSymbolMenu.addAction(action) contextMenu.insertMenu(contextMenu.actions()[0], insertSymbolMenu) if not self.textCursor().hasSelection() and spellMenu: # Select the word under the cursor for spellchecker cursor = self.textCursor() cursor.select(QTextCursor.WordUnderCursor) self.setTextCursor(cursor) text = unicode(self.textCursor().selectedText()) #TODO: put to configuration list of ignored starting/ending chars # remove u"„" from selection if text.startswith(u"„") or text.startswith(u"“"): text = text[1:] selectionEnd = cursor.selectionEnd() cursor.setPosition(cursor.position() - len(text)) cursor.setPosition(selectionEnd, QTextCursor.KeepAnchor) self.setTextCursor(cursor) # remove u"”" from selection if text.endswith(u"”") or text.startswith(u"“"): selectionEnd = cursor.selectionEnd() cursor.setPosition(cursor.position() - len(text)) cursor.setPosition(selectionEnd - 1, QTextCursor.KeepAnchor) text = text[:-1] self.setTextCursor(cursor) # Check if the selected word is misspelled and offer spelling # suggestions if it is. if self.textCursor().hasSelection(): if not self.dict.check(text): spell_menu = QMenu(self.tr("Spelling Suggestions")) addWordAcction = QAction(self.tr('Add word...'), spell_menu) QtCore.QObject.connect(addWordAcction, QtCore.SIGNAL("triggered()"), self.addWord) #addWordAcction.triggered.connect(self.addWord) spell_menu.addAction(addWordAcction) for word in self.dict.suggest(text): action = SpellAction(word, spell_menu) action.correct.connect(self.changeText) spell_menu.addAction(action) contextMenu.insertSeparator(contextMenu.actions()[1]) contextMenu.insertMenu(contextMenu.actions()[0], spell_menu) # Only add the spelling suggests to the menu if there are # suggestions. if len(spell_menu.actions()) != 1: spell_menu.insertSeparator(spell_menu.actions()[1]) contextMenu.exec_(event.globalPos()) event.accept()
def show_context_menu(self, pos): m = QMenu(self) a = m.addAction c = self.editor.cursorForPosition(pos) fmt = self.editor.syntax_format_for_cursor(c) spell = fmt.property( SPELL_PROPERTY).toPyObject() if fmt is not None else None if spell is not None: word, locale = spell orig_pos = c.position() c.setPosition(orig_pos - utf16_length(word)) found = False self.editor.setTextCursor(c) if self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False): found = True fc = self.editor.textCursor() if fc.position() < c.position(): self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False) if found: suggestions = dictionaries.suggestions(word, locale)[:7] if suggestions: for suggestion in suggestions: ac = m.addAction( suggestion, partial(self.editor.simple_replace, suggestion)) f = ac.font() f.setBold(True), ac.setFont(f) m.addSeparator() m.addAction(actions['spell-next']) m.addAction(_('Ignore this word'), partial(self._nuke_word, None, word, locale)) dics = dictionaries.active_user_dictionaries if len(dics) > 0: if len(dics) == 1: m.addAction( _('Add this word to the dictionary: {0}').format( dics[0].name), partial(self._nuke_word, dics[0].name, word, locale)) else: ac = m.addAction(_('Add this word to the dictionary')) dmenu = QMenu(m) ac.setMenu(dmenu) for dic in dics: dmenu.addAction( dic.name, partial(self._nuke_word, dic.name, word, locale)) m.addSeparator() for x in ('undo', 'redo'): a(actions['editor-%s' % x]) m.addSeparator() for x in ('cut', 'copy', 'paste'): a(actions['editor-' + x]) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) m.addAction(actions['mark-selected-text']) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.mapToGlobal(pos))
class EbookViewer(MainWindow, Ui_EbookViewer): STATE_VERSION = 1 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') def __init__(self, pathtoebook=None, debug_javascript=False, open_at=None, start_in_fullscreen=False): MainWindow.__init__(self, None) self.setupUi(self) self.view.initialize_view(debug_javascript) self.view.magnification_changed.connect(self.magnification_changed) self.show_toc_on_open = False self.current_book_has_toc = False self.base_window_title = unicode(self.windowTitle()) 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.existing_bookmarks= [] self.selected_text = None self.was_maximized = False self.read_settings() self.dictionary_box.hide() self.close_dictionary_view.clicked.connect(lambda x:self.dictionary_box.hide()) self.history = History(self.action_back, self.action_forward) self.metadata = Metadata(self) self.pos = DoubleSpinBox() self.pos.setDecimals(1) self.pos.setSuffix('/'+_('Unknown')+' ') self.pos.setMinimum(1.) self.pos.value_changed.connect(self.update_pos_label) self.splitter.setCollapsible(0, False) self.splitter.setCollapsible(1, False) self.pos.setMinimumWidth(150) self.tool_bar2.insertWidget(self.action_find_next, self.pos) self.reference = Reference() self.tool_bar2.insertSeparator(self.action_find_next) self.tool_bar2.insertWidget(self.action_find_next, self.reference) self.tool_bar2.insertSeparator(self.action_find_next) self.setFocusPolicy(Qt.StrongFocus) self.search = SearchBox2(self) self.search.setMinimumContentsLength(20) self.search.initialize('viewer_search_history') self.search.setToolTip(_('Search for text in book')) self.search.setMinimumWidth(200) self.tool_bar2.insertWidget(self.action_find_next, self.search) self.view.set_manager(self) self.pi = ProgressIndicator(self) self.toc.setVisible(False) 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_quit.triggered.connect(self.quit) self.action_copy.setDisabled(True) self.action_metadata.setCheckable(True) self.action_table_of_contents.setCheckable(True) self.toc.setMinimumWidth(80) self.action_reference_mode.setCheckable(True) 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_full_screen.setToolTip(_('Toggle full screen [%s]') % _(' or ').join([x for x in self.view.shortcuts.get_shortcuts('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.reference.goto.connect(self.goto) self.bookmarks_menu = QMenu() self.action_bookmark.setMenu(self.bookmarks_menu) self.set_bookmarks([]) self.themes_menu = QMenu() self.action_load_theme.setMenu(self.themes_menu) self.tool_bar.widgetForAction(self.action_load_theme).setPopupMode(QToolButton.InstantPopup) self.load_theme_menu() if pathtoebook is not None: f = functools.partial(self.load_ebook, pathtoebook, open_at=open_at) QTimer.singleShot(50, f) self.view.setMinimumSize(100, 100) self.toc.setCursor(Qt.PointingHandCursor) self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu) self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu) self.tool_bar.widgetForAction(self.action_bookmark).setPopupMode(QToolButton.InstantPopup) self.action_full_screen.setCheckable(True) self.full_screen_label = QLabel(''' <center> <h1>%s</h1> <h3>%s</h3> <h3>%s</h3> <h3>%s</h3> </center> '''%(_('Full screen mode'), _('Right click to show controls'), _('Tap in the left or right page margin to turn pages'), _('Press Esc to quit')), self) self.full_screen_label.setVisible(False) self.full_screen_label.setStyleSheet(''' QLabel { text-align: center; background-color: white; color: black; border-width: 1px; border-style: solid; border-radius: 20px; } ''') 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, 'size') self.clock_label = QLabel('99:99', self) self.clock_label.setVisible(False) self.clock_label.setFocusPolicy(Qt.NoFocus) self.info_label_style = ''' QLabel { text-align: center; border-width: 1px; border-style: solid; border-radius: 8px; background-color: %s; color: %s; font-family: monospace; font-size: larger; padding: 5px; }''' self.original_frame_style = self.frame.frameStyle() self.pos_label = QLabel('2000/4000', self) self.pos_label.setVisible(False) self.pos_label.setFocusPolicy(Qt.NoFocus) self.clock_timer = QTimer(self) self.clock_timer.timeout.connect(self.update_clock) self.print_menu = QMenu() self.print_menu.addAction(QIcon(I('print-preview.png')), _('Print Preview')) self.action_print.setMenu(self.print_menu) self.tool_bar.widgetForAction(self.action_print).setPopupMode(QToolButton.MenuButtonPopup) self.action_print.triggered.connect(self.print_book) self.print_menu.actions()[0].triggered.connect(self.print_preview) self.open_history_menu = QMenu() 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.action_open_ebook.setMenu(self.open_history_menu) self.open_history_menu.triggered[QAction].connect(self.open_recent) w = self.tool_bar.widgetForAction(self.action_open_ebook) w.setPopupMode(QToolButton.MenuButtonPopup) 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() 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.setVisible(yes) 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 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.shutdown(): 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['viewer_toolbar_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', bool(self.toc.isVisible())) if self.toc.isVisible(): vprefs.set('viewer_splitter_state', bytearray(self.splitter.saveState())) vprefs['multiplier'] = self.view.multiplier vprefs['in_paged_mode'] = not self.action_toggle_paged_mode.isChecked() def restore_state(self): state = vprefs.get('viewer_toolbar_state', None) if state is not None: try: state = QByteArray(state) self.restoreState(state, self.STATE_VERSION) except: pass 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.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 calibre.gui2.viewer.documentview import config opts = config().parse() settings = self.dictionary_view.page().settings() settings.setFontSize(settings.DefaultFontSize, opts.default_font_size) settings.setFontSize(settings.DefaultFixedFontSize, opts.mono_font_size) self.dictionary_view.setHtml('<html><body><p>'+ _('Connecting to dict.org to lookup: <b>%s</b>…')%word + '</p></body></html>') self.dictionary_box.show() self._lookup = Lookup(word, parent=self) self._lookup.finished.connect(self.looked_up) self._lookup.start() def looked_up(self, *args): html = self._lookup.html_result self._lookup = None self.dictionary_view.setHtml(html) def get_remember_current_page_opt(self): from calibre.gui2.viewer.documentview import config c = config().parse() return c.remember_current_page def print_book(self): p = Printing(self.iterator, self) p.start_print() def print_preview(self): p = Printing(self.iterator, self) p.start_preview() 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) self.frame.layout().setSpacing(0) self._original_frame_margins = ( self.centralwidget.layout().contentsMargins(), self.frame.layout().contentsMargins()) self.frame.layout().setContentsMargins(0, 0, 0, 0) self.centralwidget.layout().setContentsMargins(0, 0, 0, 0) self.frame.setFrameStyle(self.frame.NoFrame|self.frame.Plain) super(EbookViewer, self).showFullScreen() def show_full_screen_label(self): f = self.full_screen_label height = 200 width = int(0.7*self.view.width()) f.resize(width, height) f.move((self.view.width() - width)//2, (self.view.height()-height)//2) 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) self.view.document.switch_to_fullscreen_mode() if self.view.document.fullscreen_clock: self.show_clock() if self.view.document.fullscreen_pos: self.show_pos_label() 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()) sw = QApplication.desktop().screenGeometry(self.view) vswidth = (self.vertical_scrollbar.width() if self.vertical_scrollbar.isVisible() else 0) self.clock_label.move(sw.width() - vswidth - 15 - self.clock_label.width(), sw.height() - self.clock_label.height()-10) 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])) sw = QApplication.desktop().screenGeometry(self.view) self.pos_label.move(15, sw.height() - self.pos_label.height()-10) self.update_pos_label() 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.frame.setFrameStyle(self.original_frame_style) self.frame.layout().setSpacing(-1) self.clock_timer.stop() self.vertical_scrollbar.setVisible(True) self.window_mode_changed = 'normal' self.settings_changed() self.full_screen_label.setVisible(False) if hasattr(self, '_original_frame_margins'): om = self._original_frame_margins self.centralwidget.layout().setContentsMargins(om[0]) self.frame.layout().setContentsMargins(om[1]) if self.was_maximized: super(EbookViewer, self).showMaximized() else: super(EbookViewer, self).showNormal() def handle_window_mode_toggle(self): if self.window_mode_changed: fs = self.window_mode_changed == 'fullscreen' self.window_mode_changed = None if fs: self.show_full_screen_label() else: self.view.document.switch_to_window_mode() self.view.document.page_position.restore() self.scrolled(self.view.scroll_fraction) 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) 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, frac): self.update_page_number() # Ensure page number is accurate as it is used for history self.history.add(self.pos.value()) 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 viewport_resize_started(self, event): old, curr = event.size(), event.oldSize() if not self.window_mode_changed and old.width() == curr.width(): # No relayout changes, so page position does not need to be saved # This is needed as Qt generates a viewport resized event that # changes only the height after a file has been loaded. This can # cause the last read position bookmark to become slightly # inaccurate return 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 if self.window_mode_changed: # This resize is part of a window mode change, special case it self.handle_window_mode_toggle() else: self.view.document.page_position.restore() self.view.document.after_resize() # For some reason scroll_fraction returns incorrect results in paged # mode for some time after a resize is finished. No way of knowing # exactly how long, so we update it in a second, in the hopes that it # will be enough *most* of the time. QTimer.singleShot(1000, self.update_page_number) def update_page_number(self): self.set_page_number(self.view.document.scroll_fraction) 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() 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) def set_bookmarks(self, bookmarks): self.bookmarks_menu.clear() self.bookmarks_menu.addAction(_("Bookmark this location"), self.bookmark) self.bookmarks_menu.addAction(_("Manage Bookmarks"), self.manage_bookmarks) self.bookmarks_menu.addSeparator() current_page = None self.existing_bookmarks = [] for bm in bookmarks: if bm['title'] == 'calibre_current_page_bookmark': if self.get_remember_current_page_opt(): 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 manage_bookmarks(self): bmm = BookmarkManager(self, self.iterator.bookmarks) if bmm.exec_() != BookmarkManager.Accepted: return bookmarks = bmm.get_bookmarks() if bookmarks != self.iterator.bookmarks: self.iterator.set_bookmarks(bookmarks) self.iterator.save_bookmarks() self.set_bookmarks(bookmarks) def save_current_position(self): if not self.get_remember_current_page_opt(): return if hasattr(self, 'current_index'): try: bm = self.view.bookmark() bm['spine'] = self.current_index bm['title'] = 'calibre_current_page_bookmark' self.iterator.add_bookmark(bm) except: traceback.print_exc() def load_ebook(self, pathtoebook, open_at=None): if self.iterator is not None: self.save_current_position() self.iterator.__exit__() self.iterator = EbookIterator(pathtoebook) self.open_progress_indicator(_('Loading ebook...')) worker = Worker(target=partial(self.iterator.__enter__, extract_embedded_fonts_for_qt=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.action_table_of_contents.setDisabled(not self.iterator.toc) self.current_book_has_toc = bool(self.iterator.toc) self.current_title = title self.setWindowTitle(self.base_window_title+' - '+title + ' [%s]'%self.iterator.book_format) 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 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() 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.toggle_fullscreen() event.accept() return try: key = self.view.shortcuts.get_match(event) except AttributeError: return MainWindow.keyPressEvent(self, event) 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, }.get(key, None) if action is not None: event.accept() action.trigger() return if key == 'Focus Search': self.search.setFocus(Qt.OtherFocusReason) if not self.view.handle_key_press(event): event.ignore() 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() self.splitter.setSizes([1, 300]) if c.remember_window_size: wg = vprefs.get('viewer_window_geometry', None) if wg is not None: self.restoreGeometry(wg) ss = vprefs.get('viewer_splitter_state', None) if ss is not None: self.splitter.restoreState(ss) self.show_toc_on_open = vprefs.get('viewer_toc_isvisible', False) av = available_height() - 30 if self.height() > av: self.resize(self.width(), av) self.splitter.setCollapsible(0, False) self.splitter.setCollapsible(1, False)
class BooksView(QTableView): # {{{ files_dropped = pyqtSignal(object) add_column_signal = pyqtSignal() is_library_view = True def viewportEvent(self, event): if (event.type() == event.ToolTip and not gprefs['book_list_tooltips']): return False return QTableView.viewportEvent(self, event) def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True): QTableView.__init__(self, parent) self.gui = parent self.setProperty('highlight_current_item', 150) self.row_sizing_done = False self.alternate_views = AlternateViews(self) if not tweaks['horizontal_scrolling_per_column']: self.setHorizontalScrollMode(self.ScrollPerPixel) self.setEditTriggers(self.EditKeyPressed) if tweaks['doubleclick_on_library_view'] == 'edit_cell': self.setEditTriggers(self.DoubleClicked | self.editTriggers()) elif tweaks['doubleclick_on_library_view'] == 'open_viewer': self.setEditTriggers(self.SelectedClicked | self.editTriggers()) self.doubleClicked.connect(parent.iactions['View'].view_triggered) elif tweaks['doubleclick_on_library_view'] == 'edit_metadata': # Must not enable single-click to edit, or the field will remain # open in edit mode underneath the edit metadata dialog if use_edit_metadata_dialog: self.doubleClicked.connect( partial(parent.iactions['Edit Metadata'].edit_metadata, checked=False)) else: self.setEditTriggers(self.DoubleClicked | self.editTriggers()) setup_dnd_interface(self) self.setAlternatingRowColors(True) self.setShowGrid(False) self.setWordWrap(False) self.rating_delegate = RatingDelegate(self) self.timestamp_delegate = DateDelegate(self) self.pubdate_delegate = PubDateDelegate(self) self.last_modified_delegate = DateDelegate( self, tweak_name='gui_last_modified_display_format') self.languages_delegate = LanguagesDelegate(self) self.tags_delegate = CompleteDelegate(self, ',', 'all_tag_names') self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True) self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True) self.series_delegate = TextDelegate(self) self.publisher_delegate = TextDelegate(self) self.text_delegate = TextDelegate(self) self.cc_text_delegate = CcTextDelegate(self) self.cc_enum_delegate = CcEnumDelegate(self) self.cc_bool_delegate = CcBoolDelegate(self) self.cc_comments_delegate = CcCommentsDelegate(self) self.cc_template_delegate = CcTemplateDelegate(self) self.cc_number_delegate = CcNumberDelegate(self) self.display_parent = parent self._model = modelcls(self) self.setModel(self._model) self._model.count_changed_signal.connect(self.do_row_sizing, type=Qt.QueuedConnection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSortingEnabled(True) self.selectionModel().currentRowChanged.connect( self._model.current_changed) self.preserve_state = partial(PreserveViewState, self) self.marked_changed_listener = FunctionDispatcher(self.marked_changed) # {{{ Column Header setup self.can_add_columns = True self.was_restored = False self.column_header = HeaderView(Qt.Horizontal, self) self.setHorizontalHeader(self.column_header) self.column_header.sortIndicatorChanged.disconnect() self.column_header.sortIndicatorChanged.connect( self.user_sort_requested) self.column_header.setMovable(True) self.column_header.setClickable(True) self.column_header.sectionMoved.connect(self.save_state) self.column_header.setContextMenuPolicy(Qt.CustomContextMenu) self.column_header.customContextMenuRequested.connect( self.show_column_header_context_menu) self.column_header.sectionResized.connect(self.column_resized, Qt.QueuedConnection) self.row_header = HeaderView(Qt.Vertical, self) self.row_header.setResizeMode(self.row_header.Fixed) self.setVerticalHeader(self.row_header) # }}} self._model.database_changed.connect(self.database_changed) hv = self.verticalHeader() hv.setClickable(True) hv.setCursor(Qt.PointingHandCursor) self.selected_ids = [] self._model.about_to_be_sorted.connect(self.about_to_be_sorted) self._model.sorting_done.connect(self.sorting_done, type=Qt.QueuedConnection) # Column Header Context Menu {{{ def column_header_context_handler(self, action=None, column=None): if not action or not column: return try: idx = self.column_map.index(column) except: return h = self.column_header if action == 'hide': h.setSectionHidden(idx, True) elif action == 'show': h.setSectionHidden(idx, False) if h.sectionSize(idx) < 3: sz = h.sectionSizeHint(idx) h.resizeSection(idx, sz) elif action == 'ascending': self.sort_by_column_and_order(idx, True) elif action == 'descending': self.sort_by_column_and_order(idx, False) elif action == 'defaults': self.apply_state(self.get_default_state()) elif action == 'addcustcol': self.add_column_signal.emit() elif action.startswith('align_'): alignment = action.partition('_')[-1] self._model.change_alignment(column, alignment) elif action == 'quickview': from calibre.customize.ui import find_plugin qv = find_plugin('Show Quickview') if qv: rows = self.selectionModel().selectedRows() if len(rows) > 0: current_row = rows[0].row() current_col = self.column_map.index(column) index = self.model().index(current_row, current_col) qv.actual_plugin_.change_quickview_column(index) self.save_state() def show_column_header_context_menu(self, pos): idx = self.column_header.logicalIndexAt(pos) if idx > -1 and idx < len(self.column_map): col = self.column_map[idx] name = unicode(self.model().headerData(idx, Qt.Horizontal, Qt.DisplayRole).toString()) self.column_header_context_menu = QMenu(self) if col != 'ondevice': self.column_header_context_menu.addAction( _('Hide column %s') % name, partial(self.column_header_context_handler, action='hide', column=col)) m = self.column_header_context_menu.addMenu(_('Sort on %s') % name) a = m.addAction( _('Ascending'), partial(self.column_header_context_handler, action='ascending', column=col)) d = m.addAction( _('Descending'), partial(self.column_header_context_handler, action='descending', column=col)) if self._model.sorted_on[0] == col: ac = a if self._model.sorted_on[1] else d ac.setCheckable(True) ac.setChecked(True) if col not in ('ondevice', 'inlibrary') and \ (not self.model().is_custom_column(col) or self.model().custom_columns[col]['datatype'] not in ('bool', )): m = self.column_header_context_menu.addMenu( _('Change text alignment for %s') % name) al = self._model.alignment_map.get(col, 'left') for x, t in (('left', _('Left')), ('right', _('Right')), ('center', _('Center'))): a = m.addAction( t, partial(self.column_header_context_handler, action='align_' + x, column=col)) if al == x: a.setCheckable(True) a.setChecked(True) if not isinstance(self, DeviceBooksView): if self._model.db.field_metadata[col]['is_category']: act = self.column_header_context_menu.addAction( _('Quickview column %s') % name, partial(self.column_header_context_handler, action='quickview', column=col)) rows = self.selectionModel().selectedRows() if len(rows) > 1: act.setEnabled(False) hidden_cols = [ self.column_map[i] for i in range(self.column_header.count()) if self.column_header.isSectionHidden(i) ] try: hidden_cols.remove('ondevice') except: pass if hidden_cols: self.column_header_context_menu.addSeparator() m = self.column_header_context_menu.addMenu(_('Show column')) for col in hidden_cols: hidx = self.column_map.index(col) name = unicode(self.model().headerData( hidx, Qt.Horizontal, Qt.DisplayRole).toString()) m.addAction( name, partial(self.column_header_context_handler, action='show', column=col)) self.column_header_context_menu.addSeparator() self.column_header_context_menu.addAction( _('Shrink column if it is too wide to fit'), partial(self.resize_column_to_fit, column=self.column_map[idx])) self.column_header_context_menu.addAction( _('Restore default layout'), partial(self.column_header_context_handler, action='defaults', column=col)) if self.can_add_columns: self.column_header_context_menu.addAction( QIcon(I('column.png')), _('Add your own columns'), partial(self.column_header_context_handler, action='addcustcol', column=col)) self.column_header_context_menu.popup( self.column_header.mapToGlobal(pos)) # }}} # Sorting {{{ def sort_by_column_and_order(self, col, ascending): self.column_header.blockSignals(True) self.sortByColumn( col, Qt.AscendingOrder if ascending else Qt.DescendingOrder) self.column_header.blockSignals(False) def user_sort_requested(self, col, order=Qt.AscendingOrder): if col >= len(self.column_map) or col < 0: return QTableView.sortByColumn(self, col) field = self.column_map[col] self.intelligent_sort(field, order == Qt.AscendingOrder) def intelligent_sort(self, field, ascending): m = self.model() pname = 'previous_sort_order_' + self.__class__.__name__ previous = gprefs.get(pname, {}) if field == m.sorted_on[0] or field not in previous: self.sort_by_named_field(field, ascending) previous[field] = ascending gprefs[pname] = previous return previous[m.sorted_on[0]] = m.sorted_on[1] gprefs[pname] = previous self.sort_by_named_field(field, previous[field]) def about_to_be_sorted(self, idc): selected_rows = [r.row() for r in self.selectionModel().selectedRows()] self.selected_ids = [idc(r) for r in selected_rows] def sorting_done(self, indexc): pos = self.horizontalScrollBar().value() self.select_rows(self.selected_ids, using_ids=True, change_current=True, scroll=True) self.selected_ids = [] self.horizontalScrollBar().setValue(pos) def sort_by_named_field(self, field, order, reset=True): if field in self.column_map: idx = self.column_map.index(field) self.sort_by_column_and_order(idx, order) else: self._model.sort_by_named_field(field, order, reset) self.column_header.blockSignals(True) self.column_header.setSortIndicator(-1, Qt.AscendingOrder) self.column_header.blockSignals(False) def multisort(self, fields, reset=True, only_if_different=False): if len(fields) == 0: return sh = self.cleanup_sort_history(self._model.sort_history, ignore_column_map=True) if only_if_different and len(sh) >= len(fields): ret = True for i, t in enumerate(fields): if t[0] != sh[i][0]: ret = False break if ret: return for n, d in reversed(fields): if n in self._model.db.field_metadata.keys(): sh.insert(0, (n, d)) sh = self.cleanup_sort_history(sh, ignore_column_map=True) self._model.sort_history = [tuple(x) for x in sh] self._model.resort(reset=reset) col = fields[0][0] dir = Qt.AscendingOrder if fields[0][1] else Qt.DescendingOrder if col in self.column_map: col = self.column_map.index(col) self.column_header.blockSignals(True) try: self.column_header.setSortIndicator(col, dir) finally: self.column_header.blockSignals(False) # }}} # Ondevice column {{{ def set_ondevice_column_visibility(self): m = self._model self.column_header.setSectionHidden(m.column_map.index('ondevice'), not m.device_connected) def set_device_connected(self, is_connected): self._model.set_device_connected(is_connected) self.set_ondevice_column_visibility() # }}} # Save/Restore State {{{ def get_state(self): h = self.column_header cm = self.column_map state = {} state['hidden_columns'] = [ cm[i] for i in range(h.count()) if h.isSectionHidden(i) and cm[i] != 'ondevice' ] state['last_modified_injected'] = True state['languages_injected'] = True state['sort_history'] = \ self.cleanup_sort_history(self.model().sort_history, ignore_column_map=self.is_library_view) state['column_positions'] = {} state['column_sizes'] = {} state['column_alignment'] = self._model.alignment_map for i in range(h.count()): name = cm[i] state['column_positions'][name] = h.visualIndex(i) if name != 'ondevice': state['column_sizes'][name] = h.sectionSize(i) return state def write_state(self, state): db = getattr(self.model(), 'db', None) name = unicode(self.objectName()) if name and db is not None: db.prefs.set(name + ' books view state', state) def save_state(self): # Only save if we have been initialized (set_database called) if len(self.column_map) > 0 and self.was_restored: state = self.get_state() self.write_state(state) def cleanup_sort_history(self, sort_history, ignore_column_map=False): history = [] for col, order in sort_history: if not isinstance(order, bool): continue col = {'date': 'timestamp', 'sort': 'title'}.get(col, col) if ignore_column_map or col in self.column_map: if (not history or history[-1][0] != col): history.append([col, order]) return history def apply_sort_history(self, saved_history, max_sort_levels=3): if not saved_history: return if self.is_library_view: for col, order in reversed( self.cleanup_sort_history( saved_history, ignore_column_map=True)[:max_sort_levels]): self.sort_by_named_field(col, order) else: for col, order in reversed( self.cleanup_sort_history(saved_history) [:max_sort_levels]): self.sort_by_column_and_order(self.column_map.index(col), order) def apply_state(self, state, max_sort_levels=3): h = self.column_header cmap = {} hidden = state.get('hidden_columns', []) for i, c in enumerate(self.column_map): cmap[c] = i if c != 'ondevice': h.setSectionHidden(i, c in hidden) positions = state.get('column_positions', {}) pmap = {} for col, pos in positions.items(): if col in cmap: pmap[pos] = col for pos in sorted(pmap.keys()): col = pmap[pos] idx = cmap[col] current_pos = h.visualIndex(idx) if current_pos != pos: h.moveSection(current_pos, pos) sizes = state.get('column_sizes', {}) for col, size in sizes.items(): if col in cmap: sz = sizes[col] if sz < 3: sz = h.sectionSizeHint(cmap[col]) h.resizeSection(cmap[col], sz) self.apply_sort_history(state.get('sort_history', None), max_sort_levels=max_sort_levels) for col, alignment in state.get('column_alignment', {}).items(): self._model.change_alignment(col, alignment) for i in range(h.count()): if not h.isSectionHidden(i) and h.sectionSize(i) < 3: sz = h.sectionSizeHint(i) h.resizeSection(i, sz) def get_default_state(self): old_state = { 'hidden_columns': ['last_modified', 'languages'], 'sort_history': [DEFAULT_SORT], 'column_positions': {}, 'column_sizes': {}, 'column_alignment': { 'size': 'center', 'timestamp': 'center', 'pubdate': 'center' }, 'last_modified_injected': True, 'languages_injected': True, } h = self.column_header cm = self.column_map for i in range(h.count()): name = cm[i] old_state['column_positions'][name] = i if name != 'ondevice': old_state['column_sizes'][name] = \ min(350, max(self.sizeHintForColumn(i), h.sectionSizeHint(i))) if name in ('timestamp', 'last_modified'): old_state['column_sizes'][name] += 12 return old_state def get_old_state(self): ans = None name = unicode(self.objectName()) if name: name += ' books view state' db = getattr(self.model(), 'db', None) if db is not None: ans = db.prefs.get(name, None) if ans is None: ans = gprefs.get(name, None) try: del gprefs[name] except: pass if ans is not None: db.prefs[name] = ans else: injected = False if not ans.get('last_modified_injected', False): injected = True ans['last_modified_injected'] = True hc = ans.get('hidden_columns', []) if 'last_modified' not in hc: hc.append('last_modified') if not ans.get('languages_injected', False): injected = True ans['languages_injected'] = True hc = ans.get('hidden_columns', []) if 'languages' not in hc: hc.append('languages') if injected: db.prefs[name] = ans return ans def restore_state(self): old_state = self.get_old_state() if old_state is None: old_state = self.get_default_state() max_levels = 3 if tweaks['sort_columns_at_startup'] is not None: sh = [] try: for c, d in tweaks['sort_columns_at_startup']: if not isinstance(d, bool): d = True if d == 0 else False sh.append((c, d)) except: # Ignore invalid tweak values as users seem to often get them # wrong print( 'Ignoring invalid sort_columns_at_startup tweak, with error:' ) import traceback traceback.print_exc() old_state['sort_history'] = sh max_levels = max(3, len(sh)) self.column_header.blockSignals(True) self.apply_state(old_state, max_sort_levels=max_levels) self.column_header.blockSignals(False) self.do_row_sizing() self.was_restored = True def refresh_row_sizing(self): self.row_sizing_done = False self.do_row_sizing() def do_row_sizing(self): # Resize all rows to have the correct height if not self.row_sizing_done and self.model().rowCount( QModelIndex()) > 0: self.resizeRowToContents(0) self.verticalHeader().setDefaultSectionSize( self.rowHeight(0) + gprefs['extra_row_spacing']) self._model.set_row_height(self.rowHeight(0)) self.row_sizing_done = True def resize_column_to_fit(self, column): col = self.column_map.index(column) self.column_resized(col, self.columnWidth(col), self.columnWidth(col)) def column_resized(self, col, old_size, new_size): # arbitrary: scroll bar + header + some max_width = self.width() - (self.verticalScrollBar().width() + self.verticalHeader().width() + 10) if max_width < 200: max_width = 200 if new_size > max_width: self.column_header.blockSignals(True) self.setColumnWidth(col, max_width) self.column_header.blockSignals(False) # }}} # Initialization/Delegate Setup {{{ def set_database(self, db): self.alternate_views.set_database(db) self.save_state() self._model.set_database(db) self.tags_delegate.set_database(db) self.cc_names_delegate.set_database(db) self.authors_delegate.set_database(db) self.series_delegate.set_auto_complete_function(db.all_series) self.publisher_delegate.set_auto_complete_function(db.all_publishers) self.alternate_views.set_database(db, stage=1) def marked_changed(self, old_marked, current_marked): self.alternate_views.marked_changed(old_marked, current_marked) if bool(old_marked) == bool(current_marked): changed = old_marked | current_marked i = self.model().db.data.id_to_index def f(x): try: return i(x) except ValueError: pass sections = tuple(x for x in map(f, changed) if x is not None) if sections: self.row_header.headerDataChanged(Qt.Vertical, min(sections), max(sections)) else: # Marked items have either appeared or all been removed self.model().set_row_decoration(current_marked) self.row_header.headerDataChanged(Qt.Vertical, 0, self.row_header.count() - 1) self.row_header.geometriesChanged.emit() def database_changed(self, db): db.data.add_marked_listener(self.marked_changed_listener) for i in range(self.model().columnCount(None)): if self.itemDelegateForColumn(i) in (self.rating_delegate, self.timestamp_delegate, self.pubdate_delegate, self.last_modified_delegate, self.languages_delegate): self.setItemDelegateForColumn(i, self.itemDelegate()) cm = self.column_map for colhead in cm: if self._model.is_custom_column(colhead): cc = self._model.custom_columns[colhead] if cc['datatype'] == 'datetime': delegate = CcDateDelegate(self) delegate.set_format(cc['display'].get('date_format', '')) self.setItemDelegateForColumn(cm.index(colhead), delegate) elif cc['datatype'] == 'comments': self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate) elif cc['datatype'] == 'text': if cc['is_multiple']: if cc['display'].get('is_names', False): self.setItemDelegateForColumn( cm.index(colhead), self.cc_names_delegate) else: self.setItemDelegateForColumn( cm.index(colhead), self.tags_delegate) else: self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate) elif cc['datatype'] == 'series': self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate) elif cc['datatype'] in ('int', 'float'): self.setItemDelegateForColumn(cm.index(colhead), self.cc_number_delegate) elif cc['datatype'] == 'bool': self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate) elif cc['datatype'] == 'rating': self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate) elif cc['datatype'] == 'composite': self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate) elif cc['datatype'] == 'enumeration': self.setItemDelegateForColumn(cm.index(colhead), self.cc_enum_delegate) else: dattr = colhead + '_delegate' delegate = colhead if hasattr(self, dattr) else 'text' self.setItemDelegateForColumn( cm.index(colhead), getattr(self, delegate + '_delegate')) self.restore_state() self.set_ondevice_column_visibility() #}}} # Context Menu {{{ def set_context_menu(self, menu, edit_collections_action): self.setContextMenuPolicy(Qt.DefaultContextMenu) self.context_menu = menu self.alternate_views.set_context_menu(menu) self.edit_collections_action = edit_collections_action def contextMenuEvent(self, event): sac = self.gui.iactions['Sort By'] sort_added = tuple(ac for ac in self.context_menu.actions() if ac is sac.qaction) if sort_added: sac.update_menu() self.context_menu.popup(event.globalPos()) event.accept() # }}} @property def column_map(self): return self._model.column_map @property def visible_columns(self): h = self.horizontalHeader() logical_indices = (x for x in xrange(h.count()) if not h.isSectionHidden(x)) rmap = {i: x for i, x in enumerate(self.column_map)} return (rmap[h.visualIndex(x)] for x in logical_indices if h.visualIndex(x) > -1) def refresh_book_details(self): idx = self.currentIndex() if idx.isValid(): self._model.current_changed(idx, idx) return True return False def scrollContentsBy(self, dx, dy): # Needed as Qt bug causes headerview to not always update when scrolling QTableView.scrollContentsBy(self, dx, dy) if dy != 0: self.column_header.update() def scroll_to_row(self, row): if row > -1: h = self.horizontalHeader() for i in range(h.count()): if not h.isSectionHidden( i) and h.sectionViewportPosition(i) >= 0: self.scrollTo(self.model().index(row, i), self.PositionAtCenter) break @property def current_book(self): ci = self.currentIndex() if ci.isValid(): try: return self.model().db.data.index_to_id(ci.row()) except (IndexError, ValueError, KeyError, TypeError, AttributeError): pass def current_book_state(self): return self.current_book, self.horizontalScrollBar().value() def restore_current_book_state(self, state): book_id, hpos = state try: row = self.model().db.data.id_to_index(book_id) except (IndexError, ValueError, KeyError, TypeError, AttributeError): return self.set_current_row(row) self.scroll_to_row(row) self.horizontalScrollBar().setValue(hpos) def set_current_row(self, row=0, select=True, for_sync=False): if row > -1 and row < self.model().rowCount(QModelIndex()): h = self.horizontalHeader() logical_indices = list(range(h.count())) logical_indices = [ x for x in logical_indices if not h.isSectionHidden(x) ] pairs = [(x, h.visualIndex(x)) for x in logical_indices if h.visualIndex(x) > -1] if not pairs: pairs = [(0, 0)] pairs.sort(cmp=lambda x, y: cmp(x[1], y[1])) i = pairs[0][0] index = self.model().index(row, i) if for_sync: sm = self.selectionModel() sm.setCurrentIndex(index, sm.NoUpdate) else: self.setCurrentIndex(index) if select: sm = self.selectionModel() sm.select(index, sm.ClearAndSelect | sm.Rows) def row_at_top(self): pos = 0 while pos < 100: ans = self.rowAt(pos) if ans > -1: return ans pos += 5 def row_at_bottom(self): pos = self.viewport().height() limit = pos - 100 while pos > limit: ans = self.rowAt(pos) if ans > -1: return ans pos -= 5 def moveCursor(self, action, modifiers): orig = self.currentIndex() index = QTableView.moveCursor(self, action, modifiers) if action == QTableView.MovePageDown: moved = index.row() - orig.row() try: rows = self.row_at_bottom() - self.row_at_top() except TypeError: rows = moved if moved > rows: index = self.model().index(orig.row() + rows, index.column()) elif action == QTableView.MovePageUp: moved = orig.row() - index.row() try: rows = self.row_at_bottom() - self.row_at_top() except TypeError: rows = moved if moved > rows: index = self.model().index(orig.row() - rows, index.column()) elif action == QTableView.MoveHome and modifiers & Qt.ControlModifier: return self.model().index(0, orig.column()) elif action == QTableView.MoveEnd and modifiers & Qt.ControlModifier: return self.model().index(self.model().rowCount(QModelIndex()) - 1, orig.column()) return index def ids_to_rows(self, ids): row_map = OrderedDict() ids = frozenset(ids) m = self.model() for row in xrange(m.rowCount(QModelIndex())): if len(row_map) >= len(ids): break c = m.id(row) if c in ids: row_map[c] = row return row_map def select_rows(self, identifiers, using_ids=True, change_current=True, scroll=True): ''' Select rows identified by identifiers. identifiers can be a set of ids, row numbers or QModelIndexes. ''' rows = set([x.row() if hasattr(x, 'row') else x for x in identifiers]) if using_ids: rows = set([]) identifiers = set(identifiers) m = self.model() for row in xrange(m.rowCount(QModelIndex())): if m.id(row) in identifiers: rows.add(row) rows = list(sorted(rows)) if rows: row = rows[0] if change_current: self.set_current_row(row, select=False) if scroll: self.scroll_to_row(row) sm = self.selectionModel() sel = QItemSelection() m = self.model() max_col = m.columnCount(QModelIndex()) - 1 # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda (i, x): i - x): group = list(map(operator.itemgetter(1), g)) sel.merge( QItemSelection(m.index(min(group), 0), m.index(max(group), max_col)), sm.Select) sm.select(sel, sm.ClearAndSelect)
class ImageManager(QWidget): """An ImageManager manages a stack of images (and associated ImageControllers)""" def __init__(self, *args): QWidget.__init__(self, *args) # init layout self._lo = QVBoxLayout(self) self._lo.setContentsMargins(0, 0, 0, 0) self._lo.setSpacing(0) # init internal state self._currier = PersistentCurrier() self._z0 = 0 # z-depth of first image, the rest count down from it self._updating_imap = False self._locked_display_range = False self._imagecons = [] self._imagecon_loadorder = [] self._center_image = None self._plot = None self._border_pen = None self._drawing_key = None self._load_image_dialog = None self._model_imagecons = set() # init menu and standard actions self._menu = QMenu("&Image", self) qag = QActionGroup(self) # exclusive controls for plotting topmost or all images self._qa_plot_top = qag.addAction("Display topmost image only") self._qa_plot_all = qag.addAction("Display all images") self._qa_plot_top.setCheckable(True) self._qa_plot_all.setCheckable(True) self._qa_plot_top.setChecked(True) QObject.connect(self._qa_plot_all, SIGNAL("toggled(bool)"), self._displayAllImages) self._closing = False self._qa_load_clipboard = None self._clipboard_mode = QClipboard.Clipboard QObject.connect(QApplication.clipboard(), SIGNAL("changed(QClipboard::Mode)"), self._checkClipboardPath) # populate the menu self._repopulateMenu() def close(self): dprint(1, "closing Manager") self._closing = True for ic in self._imagecons: ic.close() def loadImage(self, filename=None, duplicate=True, to_top=True, model=None): """Loads image. Returns ImageControlBar object. If image is already loaded: returns old ICB if duplicate=False (raises to top if to_top=True), or else makes a new control bar. If model is set to a source name, marks the image as associated with a model source. These can be unloaded en masse by calling unloadModelImages(). """ if filename is None: if not self._load_image_dialog: dialog = self._load_image_dialog = QFileDialog( self, "Load FITS image", ".", "FITS images (%s);;All files (*)" % (" ".join(["*" + ext for ext in FITS_ExtensionList]))) dialog.setFileMode(QFileDialog.ExistingFile) dialog.setModal(True) QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self.loadImage) self._load_image_dialog.exec_() return None if isinstance(filename, QStringList): filename = filename[0] filename = str(filename) # report error if image does not exist if not os.path.exists(filename): self.showErrorMessage("""FITS image %s does not exist.""" % filename) return None # see if image is already loaded if not duplicate: for ic in self._imagecons: if ic.getFilename() and os.path.samefile( filename, ic.getFilename()): if to_top: self.raiseImage(ic) if model: self._model_imagecons.add(id(ic)) return ic # load the FITS image busy = BusyIndicator() dprint(2, "reading FITS image", filename) self.showMessage("""Reading FITS image %s""" % filename, 3000) QApplication.flush() try: image = SkyImage.FITSImagePlotItem(str(filename)) except KeyboardInterrupt: raise except: busy = None traceback.print_exc() self.showErrorMessage( """<P>Error loading FITS image %s: %s. This may be due to a bug in Tigger; if the FITS file loads fine in another viewer, please send the FITS file, along with a copy of any error messages from the text console, to [email protected].</P>""" % (filename, str(sys.exc_info()[1]))) return None # create control bar, add to widget stack ic = self._createImageController(image, "model source '%s'" % model if model else filename, model or image.name, model=model) self.showMessage("""Loaded FITS image %s""" % filename, 3000) dprint(2, "image loaded") return ic def showMessage(self, message, time=None): self.emit(SIGNAL("showMessage"), message, time) def showErrorMessage(self, message, time=None): self.emit(SIGNAL("showErrorMessage"), message, time) def setZ0(self, z0): self._z0 = z0 if self._imagecons: self.raiseImage(self._imagecons[0]) def enableImageBorders(self, border_pen, label_color, label_bg_brush): self._border_pen, self._label_color, self._label_bg_brush = \ border_pen, label_color, label_bg_brush def lockAllDisplayRanges(self, rc0): """Locks all display ranges, and sets the intensity from rc0""" if not self._updating_imap: self._updating_imap = True rc0.lockDisplayRange() try: for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc0: rc1.setDisplayRange(*rc0.displayRange()) rc1.lockDisplayRange() finally: self._updating_imap = False def unlockAllDisplayRanges(self): """Unlocks all display range.""" for ic in self._imagecons: ic.renderControl().lockDisplayRange(False) def _lockDisplayRange(self, rc0, lock): """Locks or unlocks the display range of a specific controller.""" if lock and not self._updating_imap: self._updating_imap = True try: # if something is already locked, copy display range from it for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc0 and rc1.isDisplayRangeLocked(): rc0.setDisplayRange(*rc1.displayRange()) finally: self._updating_imap = False def _updateDisplayRange(self, rc, dmin, dmax): """This is called whenever one of the images (or rather, its associated RenderControl object) changes its display range.""" if not rc.isDisplayRangeLocked(): return # If the display range is locked, propagate it to all images. # but don't do it if we're already propagating (otherwise we may get called in an infinte loop) if not self._updating_imap: self._updating_imap = True try: for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc and rc1.isDisplayRangeLocked(): rc1.setDisplayRange(dmin, dmax) finally: self._updating_imap = False def getImages(self): return [ic.image for ic in self._imagecons] def getTopImage(self): return (self._imagecons or None) and self._imagecons[0].image def cycleImages(self): index = self._imagecon_loadorder.index(self._imagecons[0]) index = (index + 1) % len(self._imagecon_loadorder) self.raiseImage(self._imagecon_loadorder[index]) def blinkImages(self): if len(self._imagecons) > 1: self.raiseImage(self._imagecons[1]) def incrementSlice(self, extra_axis, incr): if self._imagecons: rc = self._imagecons[0].renderControl() sliced_axes = rc.slicedAxes() if extra_axis < len(sliced_axes): rc.incrementSlice(sliced_axes[extra_axis][0], incr) def setLMRectSubset(self, rect): if self._imagecons: self._imagecons[0].setLMRectSubset(rect) def getLMRectStats(self, rect): if self._imagecons: return self._imagecons[0].renderControl().getLMRectStats(rect) def unloadModelImages(self): """Unloads images associated with model (i.e. loaded with the model=True flag)""" for ic in [ ic for ic in self._imagecons if id(ic) in self._model_imagecons ]: self.unloadImage(ic) def unloadImage(self, imagecon): """Unloads the given imagecon object.""" if imagecon not in self._imagecons: return # recenter if needed self._imagecons.remove(imagecon) self._imagecon_loadorder.remove(imagecon) self._model_imagecons.discard(id(imagecon)) # reparent widget and release it imagecon.setParent(None) imagecon.close() # recenter image, if unloaded the center image if self._center_image is imagecon.image: self.centerImage(self._imagecons[0] if self._imagecons else None, emit=False) # emit signal self._repopulateMenu() self.emit(SIGNAL("imagesChanged")) if self._imagecons: self.raiseImage(self._imagecons[0]) def getCenterImage(self): return self._center_image def centerImage(self, imagecon, emit=True): self._center_image = imagecon and imagecon.image for ic in self._imagecons: ic.setPlotProjection(self._center_image.projection) if emit: self.emit(SIGNAL("imagesChanged")) def raiseImage(self, imagecon): # reshuffle image stack, if more than one image image if len(self._imagecons) > 1: busy = BusyIndicator() # reshuffle image stack self._imagecons.remove(imagecon) self._imagecons.insert(0, imagecon) # notify imagecons for i, ic in enumerate(self._imagecons): label = "%d" % (i + 1) if i else "<B>1</B>" ic.setZ(self._z0 - i * 10, top=not i, depthlabel=label, can_raise=True) # adjust visibility for j, ic in enumerate(self._imagecons): ic.setImageVisible(not j or bool(self._qa_plot_all.isChecked())) # issue replot signal self.emit(SIGNAL("imageRaised")) self.fastReplot() # else simply update labels else: self._imagecons[0].setZ(self._z0, top=True, depthlabel=None, can_raise=False) self._imagecons[0].setImageVisible(True) # update slice menus img = imagecon.image axes = imagecon.renderControl().slicedAxes() for i, (next, prev) in enumerate(self._qa_slices): next.setVisible(False) prev.setVisible(False) if i < len(axes): iaxis, name, labels = axes[i] next.setVisible(True) prev.setVisible(True) next.setText("Show next slice along %s axis" % name) prev.setText("Show previous slice along %s axis" % name) # emit signasl self.emit(SIGNAL("imageRaised"), img) def resetDrawKey(self): """Makes and sets the current plot's drawing key""" if self._plot: key = [] for ic in self._imagecons: key.append(id(ic)) key += ic.currentSlice() self._plot.setDrawingKey(tuple(key)) def fastReplot(self, *dum): """Fast replot -- called when flipping images or slices. Uses the plot cache, if possible.""" if self._plot: self.resetDrawKey() dprint(2, "calling replot", time.time() % 60) self._plot.replot() dprint(2, "replot done", time.time() % 60) def replot(self, *dum): """Proper replot -- called when an image needs to be properly redrawn. Cleares the plot's drawing cache.""" if self._plot: self._plot.clearDrawCache() self.resetDrawKey() self._plot.replot() def attachImagesToPlot(self, plot): self._plot = plot self.resetDrawKey() for ic in self._imagecons: ic.attachToPlot(plot) def getMenu(self): return self._menu def _displayAllImages(self, enabled): busy = BusyIndicator() if enabled: for ic in self._imagecons: ic.setImageVisible(True) else: self._imagecons[0].setImageVisible(True) for ic in self._imagecons[1:]: ic.setImageVisible(False) self.replot() def _checkClipboardPath(self, mode=QClipboard.Clipboard): if self._qa_load_clipboard: self._clipboard_mode = mode try: path = str(QApplication.clipboard().text(mode)) except: path = None self._qa_load_clipboard.setEnabled( bool(path and os.path.isfile(path))) def _loadClipboardPath(self): try: path = QApplication.clipboard().text(self._clipboard_mode) except: return self.loadImage(path) def _repopulateMenu(self): self._menu.clear() self._menu.addAction("&Load image...", self.loadImage, Qt.CTRL + Qt.Key_L) self._menu.addAction("&Compute image...", self.computeImage, Qt.CTRL + Qt.Key_M) self._qa_load_clipboard = self._menu.addAction( "Load from clipboard &path", self._loadClipboardPath, Qt.CTRL + Qt.Key_P) self._checkClipboardPath() if self._imagecons: self._menu.addSeparator() # add controls to cycle images and planes for i, imgcon in enumerate(self._imagecons[::-1]): self._menu.addMenu(imgcon.getMenu()) self._menu.addSeparator() if len(self._imagecons) > 1: self._menu.addAction("Cycle images", self.cycleImages, Qt.Key_F5) self._menu.addAction("Blink images", self.blinkImages, Qt.Key_F6) self._qa_slices = ((self._menu.addAction( "Next slice along axis 1", self._currier.curry(self.incrementSlice, 0, 1), Qt.Key_F7), self._menu.addAction( "Previous slice along axis 1", self._currier.curry( self.incrementSlice, 0, -1), Qt.SHIFT + Qt.Key_F7)), (self._menu.addAction( "Next slice along axis 2", self._currier.curry(self.incrementSlice, 1, 1), Qt.Key_F8), self._menu.addAction( "Previous slice along axis 2", self._currier.curry( self.incrementSlice, 1, -1), Qt.SHIFT + Qt.Key_F8))) self._menu.addSeparator() self._menu.addAction(self._qa_plot_top) self._menu.addAction(self._qa_plot_all) def computeImage(self, expression=None): """Computes image from expression (if expression is None, pops up dialog)""" if expression is None: (expression, ok) = QInputDialog.getText( self, "Compute image", """Enter an image expression to compute. Any valid numpy expression is supported, and all functions from the numpy module are available (including sub-modules such as fft). Use 'a', 'b', 'c' to refer to images. Examples: "(a+b)/2", "cos(a)+sin(b)", "a-a.mean()", "fft.fft2(a)", etc.""" ) # (expression,ok) = QInputDialog.getText(self,"Compute image","""<P>Enter an expression to compute. # Use 'a', 'b', etc. to refer to loaded images. Any valid numpy expression is supported, and all the # functions from the numpy module are available. Examples of valid expressions include "(a+b)/2", # "cos(a)+sin(b)", "a-a.mean()", etc. # </P> # """) expression = str(expression) if not ok or not expression: return # try to parse expression arglist = [(chr(ord('a') + ic.getNumber()), ic.image) for ic in self._imagecons] try: exprfunc = eval( "lambda " + (",".join([x[0] for x in arglist])) + ":" + expression, numpy.__dict__, {}) except Exception as exc: self.showErrorMessage("""Error parsing expression "%s": %s.""" % (expression, str(exc))) return None # try to evaluate expression self.showMessage("Computing expression \"%s\"" % expression, 10000) busy = BusyIndicator() QApplication.flush() # trim trivial trailing dimensions. This avoids the problem of when an NxMx1 and an NxMx1x1 arrays are added, # the result is promoted to NxMxMx1 following the numpy rules. def trimshape(shape): out = shape while out and out[-1] == 1: out = out[:-1] return out def trimarray(array): return array.reshape(trimshape(array.shape)) try: result = exprfunc(*[trimarray(x[1].data()) for x in arglist]) except Exception as exc: busy = None traceback.print_exc() self.showErrorMessage("""Error evaluating "%s": %s.""" % (expression, str(exc))) return None busy = None if type(result) != numpy.ma.masked_array and type( result) != numpy.ndarray: self.showErrorMessage( """Result of "%s" is of invalid type "%s" (array expected).""" % (expression, type(result).__name__)) return None # convert coomplex results to real if numpy.iscomplexobj(result): self.showErrorMessage( """Result of "%s" is complex. Complex images are currently not fully supported, so we'll implicitly use the absolute value instead.""" % (expression)) expression = "abs(%s)" % expression result = abs(result) # determine which image this expression can be associated with res_shape = trimshape(result.shape) arglist = [ x for x in arglist if hasattr(x[1], 'fits_header') and trimshape(x[1].data().shape) == res_shape ] if not arglist: self.showErrorMessage( """Result of "%s" has shape %s, which does not match any loaded FITS image.""" % (expression, "x".join(map(str, result.shape)))) return None # look for an image in the arglist with the same projection, and with a valid dirname # (for the where-to-save hint) template = arglist[0][1] # if all images in arglist have the same projection, then it doesn't matter what we use # else ask if len( [x for x in arglist[1:] if x[1].projection == template.projection ]) != len(arglist) - 1: options = [x[0] for x in arglist] (which, ok) = QInputDialog.getItem( self, "Compute image", "Coordinate system to use for the result of \"%s\":" % expression, options, 0, False) if not ok: return None try: template = arglist[options.index(which)][1] except: pass # create a FITS image busy = BusyIndicator() dprint(2, "creating FITS image", expression) self.showMessage("""Creating image for %s""" % expression, 3000) QApplication.flush() try: hdu = pyfits.PrimaryHDU(result.transpose(), template.fits_header) skyimage = SkyImage.FITSImagePlotItem(name=expression, filename=None, hdu=hdu) except: busy = None traceback.print_exc() self.showErrorMessage("""Error creating FITS image %s: %s""" % (expression, str(sys.exc_info()[1]))) return None # get directory name for save-to hint dirname = getattr(template, 'filename', None) if not dirname: dirnames = [ getattr(img, 'filename') for x, img in arglist if hasattr(img, 'filename') ] dirname = dirnames[0] if dirnames else None # create control bar, add to widget stack self._createImageController( skyimage, expression, expression, save=((dirname and os.path.dirname(dirname)) or ".")) self.showMessage("Created new image for %s" % expression, 3000) dprint(2, "image created") def _createImageController(self, image, name, basename, model=False, save=False): dprint(2, "creating ImageController for", name) ic = ImageController(image, self, self, name, save=save) ic.setNumber(len(self._imagecons)) self._imagecons.insert(0, ic) self._imagecon_loadorder.append(ic) if model: self._model_imagecons.add(id(ic)) self._lo.addWidget(ic) if self._border_pen: ic.addPlotBorder(self._border_pen, basename, self._label_color, self._label_bg_brush) # attach appropriate signals image.connect(SIGNAL("slice"), self.fastReplot) image.connect(SIGNAL("repaint"), self.replot) image.connect(SIGNAL("raise"), self._currier.curry(self.raiseImage, ic)) image.connect(SIGNAL("unload"), self._currier.curry(self.unloadImage, ic)) image.connect(SIGNAL("center"), self._currier.curry(self.centerImage, ic)) QObject.connect( ic.renderControl(), SIGNAL("displayRangeChanged"), self._currier.curry(self._updateDisplayRange, ic.renderControl())) QObject.connect( ic.renderControl(), SIGNAL("displayRangeLocked"), self._currier.curry(self._lockDisplayRange, ic.renderControl())) self._plot = None # add to menus dprint(2, "repopulating menus") self._repopulateMenu() # center and raise to top of stack self.raiseImage(ic) if not self._center_image: self.centerImage(ic, emit=False) else: ic.setPlotProjection(self._center_image.projection) # signal self.emit(SIGNAL("imagesChanged")) return ic
class ImageManager(QWidget): """An ImageManager manages a stack of images (and associated ImageControllers)""" def __init__(self, *args): QWidget.__init__(self, *args) # init layout self._lo = QVBoxLayout(self) self._lo.setContentsMargins(0, 0, 0, 0) self._lo.setSpacing(0) # init internal state self._currier = PersistentCurrier() self._z0 = 0; # z-depth of first image, the rest count down from it self._updating_imap = False self._locked_display_range = False self._imagecons = [] self._imagecon_loadorder = [] self._center_image = None self._plot = None self._border_pen = None self._drawing_key = None self._load_image_dialog = None self._model_imagecons = set() # init menu and standard actions self._menu = QMenu("&Image", self) qag = QActionGroup(self) # exclusive controls for plotting topmost or all images self._qa_plot_top = qag.addAction("Display topmost image only") self._qa_plot_all = qag.addAction("Display all images") self._qa_plot_top.setCheckable(True) self._qa_plot_all.setCheckable(True) self._qa_plot_top.setChecked(True) QObject.connect(self._qa_plot_all, SIGNAL("toggled(bool)"), self._displayAllImages) self._closing = False self._qa_load_clipboard = None self._clipboard_mode = QClipboard.Clipboard QObject.connect(QApplication.clipboard(), SIGNAL("changed(QClipboard::Mode)"), self._checkClipboardPath) # populate the menu self._repopulateMenu() def close(self): dprint(1, "closing Manager") self._closing = True for ic in self._imagecons: ic.close() def loadImage(self, filename=None, duplicate=True, to_top=True, model=None): """Loads image. Returns ImageControlBar object. If image is already loaded: returns old ICB if duplicate=False (raises to top if to_top=True), or else makes a new control bar. If model is set to a source name, marks the image as associated with a model source. These can be unloaded en masse by calling unloadModelImages(). """ if filename is None: if not self._load_image_dialog: dialog = self._load_image_dialog = QFileDialog(self, "Load FITS image", ".", "FITS images (%s);;All files (*)" % (" ".join( ["*" + ext for ext in FITS_ExtensionList]))) dialog.setFileMode(QFileDialog.ExistingFile) dialog.setModal(True) QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self.loadImage) self._load_image_dialog.exec_() return None if isinstance(filename, QStringList): filename = filename[0] filename = str(filename) # report error if image does not exist if not os.path.exists(filename): self.showErrorMessage("""FITS image %s does not exist.""" % filename) return None # see if image is already loaded if not duplicate: for ic in self._imagecons: if ic.getFilename() and os.path.samefile(filename, ic.getFilename()): if to_top: self.raiseImage(ic) if model: self._model_imagecons.add(id(ic)) return ic # load the FITS image busy = BusyIndicator() dprint(2, "reading FITS image", filename) self.showMessage("""Reading FITS image %s""" % filename, 3000) QApplication.flush() try: image = SkyImage.FITSImagePlotItem(str(filename)) except KeyboardInterrupt: raise except: busy = None traceback.print_exc() self.showErrorMessage("""<P>Error loading FITS image %s: %s. This may be due to a bug in Tigger; if the FITS file loads fine in another viewer, please send the FITS file, along with a copy of any error messages from the text console, to [email protected].</P>""" % ( filename, str(sys.exc_info()[1]))) return None # create control bar, add to widget stack ic = self._createImageController(image, "model source '%s'" % model if model else filename, model or image.name, model=model) self.showMessage("""Loaded FITS image %s""" % filename, 3000) dprint(2, "image loaded") return ic def showMessage(self, message, time=None): self.emit(SIGNAL("showMessage"), message, time) def showErrorMessage(self, message, time=None): self.emit(SIGNAL("showErrorMessage"), message, time) def setZ0(self, z0): self._z0 = z0 if self._imagecons: self.raiseImage(self._imagecons[0]) def enableImageBorders(self, border_pen, label_color, label_bg_brush): self._border_pen, self._label_color, self._label_bg_brush = \ border_pen, label_color, label_bg_brush def lockAllDisplayRanges(self, rc0): """Locks all display ranges, and sets the intensity from rc0""" if not self._updating_imap: self._updating_imap = True rc0.lockDisplayRange() try: for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc0: rc1.setDisplayRange(*rc0.displayRange()) rc1.lockDisplayRange() finally: self._updating_imap = False def unlockAllDisplayRanges(self): """Unlocks all display range.""" for ic in self._imagecons: ic.renderControl().lockDisplayRange(False) def _lockDisplayRange(self, rc0, lock): """Locks or unlocks the display range of a specific controller.""" if lock and not self._updating_imap: self._updating_imap = True try: # if something is already locked, copy display range from it for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc0 and rc1.isDisplayRangeLocked(): rc0.setDisplayRange(*rc1.displayRange()) finally: self._updating_imap = False def _updateDisplayRange(self, rc, dmin, dmax): """This is called whenever one of the images (or rather, its associated RenderControl object) changes its display range.""" if not rc.isDisplayRangeLocked(): return # If the display range is locked, propagate it to all images. # but don't do it if we're already propagating (otherwise we may get called in an infinte loop) if not self._updating_imap: self._updating_imap = True try: for ic in self._imagecons: rc1 = ic.renderControl() if rc1 is not rc and rc1.isDisplayRangeLocked(): rc1.setDisplayRange(dmin, dmax) finally: self._updating_imap = False def getImages(self): return [ic.image for ic in self._imagecons] def getTopImage(self): return (self._imagecons or None) and self._imagecons[0].image def cycleImages(self): index = self._imagecon_loadorder.index(self._imagecons[0]) index = (index + 1) % len(self._imagecon_loadorder) self.raiseImage(self._imagecon_loadorder[index]) def blinkImages(self): if len(self._imagecons) > 1: self.raiseImage(self._imagecons[1]) def incrementSlice(self, extra_axis, incr): if self._imagecons: rc = self._imagecons[0].renderControl() sliced_axes = rc.slicedAxes() if extra_axis < len(sliced_axes): rc.incrementSlice(sliced_axes[extra_axis][0], incr) def setLMRectSubset(self, rect): if self._imagecons: self._imagecons[0].setLMRectSubset(rect) def getLMRectStats(self, rect): if self._imagecons: return self._imagecons[0].renderControl().getLMRectStats(rect) def unloadModelImages(self): """Unloads images associated with model (i.e. loaded with the model=True flag)""" for ic in [ic for ic in self._imagecons if id(ic) in self._model_imagecons]: self.unloadImage(ic) def unloadImage(self, imagecon): """Unloads the given imagecon object.""" if imagecon not in self._imagecons: return # recenter if needed self._imagecons.remove(imagecon) self._imagecon_loadorder.remove(imagecon) self._model_imagecons.discard(id(imagecon)) # reparent widget and release it imagecon.setParent(None) imagecon.close() # recenter image, if unloaded the center image if self._center_image is imagecon.image: self.centerImage(self._imagecons[0] if self._imagecons else None, emit=False) # emit signal self._repopulateMenu() self.emit(SIGNAL("imagesChanged")) if self._imagecons: self.raiseImage(self._imagecons[0]) def getCenterImage(self): return self._center_image def centerImage(self, imagecon, emit=True): self._center_image = imagecon and imagecon.image for ic in self._imagecons: ic.setPlotProjection(self._center_image.projection) if emit: self.emit(SIGNAL("imagesChanged")) def raiseImage(self, imagecon): # reshuffle image stack, if more than one image image if len(self._imagecons) > 1: busy = BusyIndicator() # reshuffle image stack self._imagecons.remove(imagecon) self._imagecons.insert(0, imagecon) # notify imagecons for i, ic in enumerate(self._imagecons): label = "%d" % (i + 1) if i else "<B>1</B>" ic.setZ(self._z0 - i * 10, top=not i, depthlabel=label, can_raise=True) # adjust visibility for j, ic in enumerate(self._imagecons): ic.setImageVisible(not j or bool(self._qa_plot_all.isChecked())) # issue replot signal self.emit(SIGNAL("imageRaised")) self.fastReplot() # else simply update labels else: self._imagecons[0].setZ(self._z0, top=True, depthlabel=None, can_raise=False) self._imagecons[0].setImageVisible(True) # update slice menus img = imagecon.image axes = imagecon.renderControl().slicedAxes() for i, (next, prev) in enumerate(self._qa_slices): next.setVisible(False) prev.setVisible(False) if i < len(axes): iaxis, name, labels = axes[i] next.setVisible(True) prev.setVisible(True) next.setText("Show next slice along %s axis" % name) prev.setText("Show previous slice along %s axis" % name) # emit signasl self.emit(SIGNAL("imageRaised"), img) def resetDrawKey(self): """Makes and sets the current plot's drawing key""" if self._plot: key = [] for ic in self._imagecons: key.append(id(ic)) key += ic.currentSlice() self._plot.setDrawingKey(tuple(key)) def fastReplot(self, *dum): """Fast replot -- called when flipping images or slices. Uses the plot cache, if possible.""" if self._plot: self.resetDrawKey() dprint(2, "calling replot", time.time() % 60) self._plot.replot() dprint(2, "replot done", time.time() % 60) def replot(self, *dum): """Proper replot -- called when an image needs to be properly redrawn. Cleares the plot's drawing cache.""" if self._plot: self._plot.clearDrawCache() self.resetDrawKey() self._plot.replot() def attachImagesToPlot(self, plot): self._plot = plot self.resetDrawKey() for ic in self._imagecons: ic.attachToPlot(plot) def getMenu(self): return self._menu def _displayAllImages(self, enabled): busy = BusyIndicator() if enabled: for ic in self._imagecons: ic.setImageVisible(True) else: self._imagecons[0].setImageVisible(True) for ic in self._imagecons[1:]: ic.setImageVisible(False) self.replot() def _checkClipboardPath(self, mode=QClipboard.Clipboard): if self._qa_load_clipboard: self._clipboard_mode = mode try: path = str(QApplication.clipboard().text(mode)) except: path = None self._qa_load_clipboard.setEnabled(bool(path and os.path.isfile(path))) def _loadClipboardPath(self): try: path = QApplication.clipboard().text(self._clipboard_mode) except: return self.loadImage(path) def _repopulateMenu(self): self._menu.clear() self._menu.addAction("&Load image...", self.loadImage, Qt.CTRL + Qt.Key_L) self._menu.addAction("&Compute image...", self.computeImage, Qt.CTRL + Qt.Key_M) self._qa_load_clipboard = self._menu.addAction("Load from clipboard &path", self._loadClipboardPath, Qt.CTRL + Qt.Key_P) self._checkClipboardPath() if self._imagecons: self._menu.addSeparator() # add controls to cycle images and planes for i, imgcon in enumerate(self._imagecons[::-1]): self._menu.addMenu(imgcon.getMenu()) self._menu.addSeparator() if len(self._imagecons) > 1: self._menu.addAction("Cycle images", self.cycleImages, Qt.Key_F5) self._menu.addAction("Blink images", self.blinkImages, Qt.Key_F6) self._qa_slices = ( (self._menu.addAction("Next slice along axis 1", self._currier.curry(self.incrementSlice, 0, 1), Qt.Key_F7), self._menu.addAction("Previous slice along axis 1", self._currier.curry(self.incrementSlice, 0, -1), Qt.SHIFT + Qt.Key_F7)), (self._menu.addAction("Next slice along axis 2", self._currier.curry(self.incrementSlice, 1, 1), Qt.Key_F8), self._menu.addAction("Previous slice along axis 2", self._currier.curry(self.incrementSlice, 1, -1), Qt.SHIFT + Qt.Key_F8))) self._menu.addSeparator() self._menu.addAction(self._qa_plot_top) self._menu.addAction(self._qa_plot_all) def computeImage(self, expression=None): """Computes image from expression (if expression is None, pops up dialog)""" if expression is None: (expression, ok) = QInputDialog.getText(self, "Compute image", """Enter an image expression to compute. Any valid numpy expression is supported, and all functions from the numpy module are available (including sub-modules such as fft). Use 'a', 'b', 'c' to refer to images. Examples: "(a+b)/2", "cos(a)+sin(b)", "a-a.mean()", "fft.fft2(a)", etc.""") # (expression,ok) = QInputDialog.getText(self,"Compute image","""<P>Enter an expression to compute. # Use 'a', 'b', etc. to refer to loaded images. Any valid numpy expression is supported, and all the # functions from the numpy module are available. Examples of valid expressions include "(a+b)/2", # "cos(a)+sin(b)", "a-a.mean()", etc. # </P> # """) expression = str(expression) if not ok or not expression: return # try to parse expression arglist = [(chr(ord('a') + ic.getNumber()), ic.image) for ic in self._imagecons] try: exprfunc = eval("lambda " + (",".join([x[0] for x in arglist])) + ":" + expression, numpy.__dict__, {}) except Exception as exc: self.showErrorMessage("""Error parsing expression "%s": %s.""" % (expression, str(exc))) return None # try to evaluate expression self.showMessage("Computing expression \"%s\"" % expression, 10000) busy = BusyIndicator() QApplication.flush() # trim trivial trailing dimensions. This avoids the problem of when an NxMx1 and an NxMx1x1 arrays are added, # the result is promoted to NxMxMx1 following the numpy rules. def trimshape(shape): out = shape while out and out[-1] == 1: out = out[:-1] return out def trimarray(array): return array.reshape(trimshape(array.shape)) try: result = exprfunc(*[trimarray(x[1].data()) for x in arglist]) except Exception as exc: busy = None traceback.print_exc() self.showErrorMessage("""Error evaluating "%s": %s.""" % (expression, str(exc))) return None busy = None if type(result) != numpy.ma.masked_array and type(result) != numpy.ndarray: self.showErrorMessage( """Result of "%s" is of invalid type "%s" (array expected).""" % (expression, type(result).__name__)) return None # convert coomplex results to real if numpy.iscomplexobj(result): self.showErrorMessage("""Result of "%s" is complex. Complex images are currently not fully supported, so we'll implicitly use the absolute value instead.""" % (expression)) expression = "abs(%s)" % expression result = abs(result) # determine which image this expression can be associated with res_shape = trimshape(result.shape) arglist = [x for x in arglist if hasattr(x[1], 'fits_header') and trimshape(x[1].data().shape) == res_shape] if not arglist: self.showErrorMessage("""Result of "%s" has shape %s, which does not match any loaded FITS image.""" % ( expression, "x".join(map(str, result.shape)))) return None # look for an image in the arglist with the same projection, and with a valid dirname # (for the where-to-save hint) template = arglist[0][1] # if all images in arglist have the same projection, then it doesn't matter what we use # else ask if len([x for x in arglist[1:] if x[1].projection == template.projection]) != len(arglist) - 1: options = [x[0] for x in arglist] (which, ok) = QInputDialog.getItem(self, "Compute image", "Coordinate system to use for the result of \"%s\":" % expression, options, 0, False) if not ok: return None try: template = arglist[options.index(which)][1] except: pass # create a FITS image busy = BusyIndicator() dprint(2, "creating FITS image", expression) self.showMessage("""Creating image for %s""" % expression, 3000) QApplication.flush() try: hdu = pyfits.PrimaryHDU(result.transpose(), template.fits_header) skyimage = SkyImage.FITSImagePlotItem(name=expression, filename=None, hdu=hdu) except: busy = None traceback.print_exc() self.showErrorMessage("""Error creating FITS image %s: %s""" % (expression, str(sys.exc_info()[1]))) return None # get directory name for save-to hint dirname = getattr(template, 'filename', None) if not dirname: dirnames = [getattr(img, 'filename') for x, img in arglist if hasattr(img, 'filename')] dirname = dirnames[0] if dirnames else None # create control bar, add to widget stack self._createImageController(skyimage, expression, expression, save=((dirname and os.path.dirname(dirname)) or ".")) self.showMessage("Created new image for %s" % expression, 3000) dprint(2, "image created") def _createImageController(self, image, name, basename, model=False, save=False): dprint(2, "creating ImageController for", name) ic = ImageController(image, self, self, name, save=save) ic.setNumber(len(self._imagecons)) self._imagecons.insert(0, ic) self._imagecon_loadorder.append(ic) if model: self._model_imagecons.add(id(ic)) self._lo.addWidget(ic) if self._border_pen: ic.addPlotBorder(self._border_pen, basename, self._label_color, self._label_bg_brush) # attach appropriate signals image.connect(SIGNAL("slice"), self.fastReplot) image.connect(SIGNAL("repaint"), self.replot) image.connect(SIGNAL("raise"), self._currier.curry(self.raiseImage, ic)) image.connect(SIGNAL("unload"), self._currier.curry(self.unloadImage, ic)) image.connect(SIGNAL("center"), self._currier.curry(self.centerImage, ic)) QObject.connect(ic.renderControl(), SIGNAL("displayRangeChanged"), self._currier.curry(self._updateDisplayRange, ic.renderControl())) QObject.connect(ic.renderControl(), SIGNAL("displayRangeLocked"), self._currier.curry(self._lockDisplayRange, ic.renderControl())) self._plot = None # add to menus dprint(2, "repopulating menus") self._repopulateMenu() # center and raise to top of stack self.raiseImage(ic) if not self._center_image: self.centerImage(ic, emit=False) else: ic.setPlotProjection(self._center_image.projection) # signal self.emit(SIGNAL("imagesChanged")) return ic
class BooksView(QTableView): # {{{ files_dropped = pyqtSignal(object) add_column_signal = pyqtSignal() def viewportEvent(self, event): if (event.type() == event.ToolTip and not gprefs['book_list_tooltips']): return False return QTableView.viewportEvent(self, event) def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True): QTableView.__init__(self, parent) self.setProperty('highlight_current_item', 150) self.row_sizing_done = False if not tweaks['horizontal_scrolling_per_column']: self.setHorizontalScrollMode(self.ScrollPerPixel) self.setEditTriggers(self.EditKeyPressed) if tweaks['doubleclick_on_library_view'] == 'edit_cell': self.setEditTriggers(self.DoubleClicked | self.editTriggers()) elif tweaks['doubleclick_on_library_view'] == 'open_viewer': self.setEditTriggers(self.SelectedClicked | self.editTriggers()) self.doubleClicked.connect(parent.iactions['View'].view_triggered) elif tweaks['doubleclick_on_library_view'] == 'edit_metadata': # Must not enable single-click to edit, or the field will remain # open in edit mode underneath the edit metadata dialog if use_edit_metadata_dialog: self.doubleClicked.connect( partial(parent.iactions['Edit Metadata'].edit_metadata, checked=False)) else: self.setEditTriggers(self.DoubleClicked | self.editTriggers()) self.drag_allowed = True self.setDragEnabled(True) self.setDragDropOverwriteMode(False) self.setDragDropMode(self.DragDrop) self.drag_start_pos = None self.setAlternatingRowColors(True) self.setSelectionBehavior(self.SelectRows) self.setShowGrid(False) self.setWordWrap(False) self.rating_delegate = RatingDelegate(self) self.timestamp_delegate = DateDelegate(self) self.pubdate_delegate = PubDateDelegate(self) self.last_modified_delegate = DateDelegate( self, tweak_name='gui_last_modified_display_format') self.languages_delegate = LanguagesDelegate(self) self.tags_delegate = CompleteDelegate(self, ',', 'all_tag_names') self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True) self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True) self.series_delegate = TextDelegate(self) self.publisher_delegate = TextDelegate(self) self.text_delegate = TextDelegate(self) self.cc_text_delegate = CcTextDelegate(self) self.cc_enum_delegate = CcEnumDelegate(self) self.cc_bool_delegate = CcBoolDelegate(self) self.cc_comments_delegate = CcCommentsDelegate(self) self.cc_template_delegate = CcTemplateDelegate(self) self.cc_number_delegate = CcNumberDelegate(self) self.display_parent = parent self._model = modelcls(self) self.setModel(self._model) self._model.count_changed_signal.connect(self.do_row_sizing, type=Qt.QueuedConnection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSortingEnabled(True) self.selectionModel().currentRowChanged.connect( self._model.current_changed) self.preserve_state = partial(PreserveViewState, self) # {{{ Column Header setup self.can_add_columns = True self.was_restored = False self.column_header = HeaderView(Qt.Horizontal, self) self.setHorizontalHeader(self.column_header) self.column_header.setMovable(True) self.column_header.setClickable(True) self.column_header.sectionMoved.connect(self.save_state) self.column_header.setContextMenuPolicy(Qt.CustomContextMenu) self.column_header.customContextMenuRequested.connect( self.show_column_header_context_menu) self.column_header.sectionResized.connect(self.column_resized, Qt.QueuedConnection) self.row_header = HeaderView(Qt.Vertical, self) self.setVerticalHeader(self.row_header) # }}} self._model.database_changed.connect(self.database_changed) hv = self.verticalHeader() hv.setClickable(True) hv.setCursor(Qt.PointingHandCursor) self.selected_ids = [] self._model.about_to_be_sorted.connect(self.about_to_be_sorted) self._model.sorting_done.connect(self.sorting_done, type=Qt.QueuedConnection) # Column Header Context Menu {{{ def column_header_context_handler(self, action=None, column=None): if not action or not column: return try: idx = self.column_map.index(column) except: return h = self.column_header if action == 'hide': h.setSectionHidden(idx, True) elif action == 'show': h.setSectionHidden(idx, False) if h.sectionSize(idx) < 3: sz = h.sectionSizeHint(idx) h.resizeSection(idx, sz) elif action == 'ascending': self.sortByColumn(idx, Qt.AscendingOrder) elif action == 'descending': self.sortByColumn(idx, Qt.DescendingOrder) elif action == 'defaults': self.apply_state(self.get_default_state()) elif action == 'addcustcol': self.add_column_signal.emit() elif action.startswith('align_'): alignment = action.partition('_')[-1] self._model.change_alignment(column, alignment) elif action == 'quickview': from calibre.customize.ui import find_plugin qv = find_plugin('Show Quickview') if qv: rows = self.selectionModel().selectedRows() if len(rows) > 0: current_row = rows[0].row() current_col = self.column_map.index(column) index = self.model().index(current_row, current_col) qv.actual_plugin_.change_quickview_column(index) self.save_state() def show_column_header_context_menu(self, pos): idx = self.column_header.logicalIndexAt(pos) if idx > -1 and idx < len(self.column_map): col = self.column_map[idx] name = unicode(self.model().headerData(idx, Qt.Horizontal, Qt.DisplayRole).toString()) self.column_header_context_menu = QMenu(self) if col != 'ondevice': self.column_header_context_menu.addAction( _('Hide column %s') % name, partial(self.column_header_context_handler, action='hide', column=col)) m = self.column_header_context_menu.addMenu(_('Sort on %s') % name) a = m.addAction( _('Ascending'), partial(self.column_header_context_handler, action='ascending', column=col)) d = m.addAction( _('Descending'), partial(self.column_header_context_handler, action='descending', column=col)) if self._model.sorted_on[0] == col: ac = a if self._model.sorted_on[1] else d ac.setCheckable(True) ac.setChecked(True) if col not in ('ondevice', 'inlibrary') and \ (not self.model().is_custom_column(col) or self.model().custom_columns[col]['datatype'] not in ('bool', )): m = self.column_header_context_menu.addMenu( _('Change text alignment for %s') % name) al = self._model.alignment_map.get(col, 'left') for x, t in (('left', _('Left')), ('right', _('Right')), ('center', _('Center'))): a = m.addAction( t, partial(self.column_header_context_handler, action='align_' + x, column=col)) if al == x: a.setCheckable(True) a.setChecked(True) if not isinstance(self, DeviceBooksView): if self._model.db.field_metadata[col]['is_category']: act = self.column_header_context_menu.addAction( _('Quickview column %s') % name, partial(self.column_header_context_handler, action='quickview', column=col)) rows = self.selectionModel().selectedRows() if len(rows) > 1: act.setEnabled(False) hidden_cols = [ self.column_map[i] for i in range(self.column_header.count()) if self.column_header.isSectionHidden(i) ] try: hidden_cols.remove('ondevice') except: pass if hidden_cols: self.column_header_context_menu.addSeparator() m = self.column_header_context_menu.addMenu(_('Show column')) for col in hidden_cols: hidx = self.column_map.index(col) name = unicode(self.model().headerData( hidx, Qt.Horizontal, Qt.DisplayRole).toString()) m.addAction( name, partial(self.column_header_context_handler, action='show', column=col)) self.column_header_context_menu.addSeparator() self.column_header_context_menu.addAction( _('Shrink column if it is too wide to fit'), partial(self.resize_column_to_fit, column=self.column_map[idx])) self.column_header_context_menu.addAction( _('Restore default layout'), partial(self.column_header_context_handler, action='defaults', column=col)) if self.can_add_columns: self.column_header_context_menu.addAction( QIcon(I('column.png')), _('Add your own columns'), partial(self.column_header_context_handler, action='addcustcol', column=col)) self.column_header_context_menu.popup( self.column_header.mapToGlobal(pos)) # }}} # Sorting {{{ def about_to_be_sorted(self, idc): selected_rows = [r.row() for r in self.selectionModel().selectedRows()] self.selected_ids = [idc(r) for r in selected_rows] def sorting_done(self, indexc): pos = self.horizontalScrollBar().value() self.select_rows(self.selected_ids, using_ids=True, change_current=True, scroll=True) self.selected_ids = [] self.horizontalScrollBar().setValue(pos) def sort_by_named_field(self, field, order, reset=True): if field in self.column_map: idx = self.column_map.index(field) if order: self.sortByColumn(idx, Qt.AscendingOrder) else: self.sortByColumn(idx, Qt.DescendingOrder) else: self._model.sort_by_named_field(field, order, reset) def multisort(self, fields, reset=True, only_if_different=False): if len(fields) == 0: return sh = self.cleanup_sort_history(self._model.sort_history, ignore_column_map=True) if only_if_different and len(sh) >= len(fields): ret = True for i, t in enumerate(fields): if t[0] != sh[i][0]: ret = False break if ret: return for n, d in reversed(fields): if n in self._model.db.field_metadata.keys(): sh.insert(0, (n, d)) sh = self.cleanup_sort_history(sh, ignore_column_map=True) self._model.sort_history = [tuple(x) for x in sh] self._model.resort(reset=reset) col = fields[0][0] dir = Qt.AscendingOrder if fields[0][1] else Qt.DescendingOrder if col in self.column_map: col = self.column_map.index(col) hdrs = self.horizontalHeader() try: hdrs.setSortIndicator(col, dir) except: pass # }}} # Ondevice column {{{ def set_ondevice_column_visibility(self): m = self._model self.column_header.setSectionHidden(m.column_map.index('ondevice'), not m.device_connected) def set_device_connected(self, is_connected): self._model.set_device_connected(is_connected) self.set_ondevice_column_visibility() # }}} # Save/Restore State {{{ def get_state(self): h = self.column_header cm = self.column_map state = {} state['hidden_columns'] = [ cm[i] for i in range(h.count()) if h.isSectionHidden(i) and cm[i] != 'ondevice' ] state['last_modified_injected'] = True state['languages_injected'] = True state['sort_history'] = \ self.cleanup_sort_history(self.model().sort_history) state['column_positions'] = {} state['column_sizes'] = {} state['column_alignment'] = self._model.alignment_map for i in range(h.count()): name = cm[i] state['column_positions'][name] = h.visualIndex(i) if name != 'ondevice': state['column_sizes'][name] = h.sectionSize(i) return state def write_state(self, state): db = getattr(self.model(), 'db', None) name = unicode(self.objectName()) if name and db is not None: db.prefs.set(name + ' books view state', state) def save_state(self): # Only save if we have been initialized (set_database called) if len(self.column_map) > 0 and self.was_restored: state = self.get_state() self.write_state(state) def cleanup_sort_history(self, sort_history, ignore_column_map=False): history = [] for col, order in sort_history: if not isinstance(order, bool): continue if col == 'date': col = 'timestamp' if ignore_column_map or col in self.column_map: if (not history or history[-1][0] != col): history.append([col, order]) return history def apply_sort_history(self, saved_history, max_sort_levels=3): if not saved_history: return for col, order in reversed( self.cleanup_sort_history(saved_history)[:max_sort_levels]): self.sortByColumn( self.column_map.index(col), Qt.AscendingOrder if order else Qt.DescendingOrder) def apply_state(self, state, max_sort_levels=3): h = self.column_header cmap = {} hidden = state.get('hidden_columns', []) for i, c in enumerate(self.column_map): cmap[c] = i if c != 'ondevice': h.setSectionHidden(i, c in hidden) positions = state.get('column_positions', {}) pmap = {} for col, pos in positions.items(): if col in cmap: pmap[pos] = col for pos in sorted(pmap.keys()): col = pmap[pos] idx = cmap[col] current_pos = h.visualIndex(idx) if current_pos != pos: h.moveSection(current_pos, pos) sizes = state.get('column_sizes', {}) for col, size in sizes.items(): if col in cmap: sz = sizes[col] if sz < 3: sz = h.sectionSizeHint(cmap[col]) h.resizeSection(cmap[col], sz) self.apply_sort_history(state.get('sort_history', None), max_sort_levels=max_sort_levels) for col, alignment in state.get('column_alignment', {}).items(): self._model.change_alignment(col, alignment) for i in range(h.count()): if not h.isSectionHidden(i) and h.sectionSize(i) < 3: sz = h.sectionSizeHint(i) h.resizeSection(i, sz) def get_default_state(self): old_state = { 'hidden_columns': ['last_modified', 'languages'], 'sort_history': [DEFAULT_SORT], 'column_positions': {}, 'column_sizes': {}, 'column_alignment': { 'size': 'center', 'timestamp': 'center', 'pubdate': 'center' }, 'last_modified_injected': True, 'languages_injected': True, } h = self.column_header cm = self.column_map for i in range(h.count()): name = cm[i] old_state['column_positions'][name] = i if name != 'ondevice': old_state['column_sizes'][name] = \ min(350, max(self.sizeHintForColumn(i), h.sectionSizeHint(i))) if name in ('timestamp', 'last_modified'): old_state['column_sizes'][name] += 12 return old_state def get_old_state(self): ans = None name = unicode(self.objectName()) if name: name += ' books view state' db = getattr(self.model(), 'db', None) if db is not None: ans = db.prefs.get(name, None) if ans is None: ans = gprefs.get(name, None) try: del gprefs[name] except: pass if ans is not None: db.prefs[name] = ans else: injected = False if not ans.get('last_modified_injected', False): injected = True ans['last_modified_injected'] = True hc = ans.get('hidden_columns', []) if 'last_modified' not in hc: hc.append('last_modified') if not ans.get('languages_injected', False): injected = True ans['languages_injected'] = True hc = ans.get('hidden_columns', []) if 'languages' not in hc: hc.append('languages') if injected: db.prefs[name] = ans return ans def restore_state(self): old_state = self.get_old_state() if old_state is None: old_state = self.get_default_state() max_levels = 3 if tweaks['sort_columns_at_startup'] is not None: sh = [] try: for c, d in tweaks['sort_columns_at_startup']: if not isinstance(d, bool): d = True if d == 0 else False sh.append((c, d)) except: # Ignore invalid tweak values as users seem to often get them # wrong print( 'Ignoring invalid sort_columns_at_startup tweak, with error:' ) import traceback traceback.print_exc() old_state['sort_history'] = sh max_levels = max(3, len(sh)) self.column_header.blockSignals(True) self.apply_state(old_state, max_sort_levels=max_levels) self.column_header.blockSignals(False) self.do_row_sizing() self.was_restored = True def refresh_row_sizing(self): self.row_sizing_done = False self.do_row_sizing() def do_row_sizing(self): # Resize all rows to have the correct height if not self.row_sizing_done and self.model().rowCount( QModelIndex()) > 0: self.resizeRowToContents(0) self.verticalHeader().setDefaultSectionSize( self.rowHeight(0) + gprefs['extra_row_spacing']) self.row_sizing_done = True def resize_column_to_fit(self, column): col = self.column_map.index(column) self.column_resized(col, self.columnWidth(col), self.columnWidth(col)) def column_resized(self, col, old_size, new_size): # arbitrary: scroll bar + header + some max_width = self.width() - (self.verticalScrollBar().width() + self.verticalHeader().width() + 10) if max_width < 200: max_width = 200 if new_size > max_width: self.column_header.blockSignals(True) self.setColumnWidth(col, max_width) self.column_header.blockSignals(False) # }}} # Initialization/Delegate Setup {{{ def set_database(self, db): self.save_state() self._model.set_database(db) self.tags_delegate.set_database(db) self.cc_names_delegate.set_database(db) self.authors_delegate.set_database(db) self.series_delegate.set_auto_complete_function(db.all_series) self.publisher_delegate.set_auto_complete_function(db.all_publishers) def database_changed(self, db): for i in range(self.model().columnCount(None)): if self.itemDelegateForColumn(i) in (self.rating_delegate, self.timestamp_delegate, self.pubdate_delegate, self.last_modified_delegate, self.languages_delegate): self.setItemDelegateForColumn(i, self.itemDelegate()) cm = self.column_map for colhead in cm: if self._model.is_custom_column(colhead): cc = self._model.custom_columns[colhead] if cc['datatype'] == 'datetime': delegate = CcDateDelegate(self) delegate.set_format(cc['display'].get('date_format', '')) self.setItemDelegateForColumn(cm.index(colhead), delegate) elif cc['datatype'] == 'comments': self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate) elif cc['datatype'] == 'text': if cc['is_multiple']: if cc['display'].get('is_names', False): self.setItemDelegateForColumn( cm.index(colhead), self.cc_names_delegate) else: self.setItemDelegateForColumn( cm.index(colhead), self.tags_delegate) else: self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate) elif cc['datatype'] == 'series': self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate) elif cc['datatype'] in ('int', 'float'): self.setItemDelegateForColumn(cm.index(colhead), self.cc_number_delegate) elif cc['datatype'] == 'bool': self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate) elif cc['datatype'] == 'rating': self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate) elif cc['datatype'] == 'composite': self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate) elif cc['datatype'] == 'enumeration': self.setItemDelegateForColumn(cm.index(colhead), self.cc_enum_delegate) else: dattr = colhead + '_delegate' delegate = colhead if hasattr(self, dattr) else 'text' self.setItemDelegateForColumn( cm.index(colhead), getattr(self, delegate + '_delegate')) self.restore_state() self.set_ondevice_column_visibility() #}}} # Context Menu {{{ def set_context_menu(self, menu, edit_collections_action): self.setContextMenuPolicy(Qt.DefaultContextMenu) self.context_menu = menu self.edit_collections_action = edit_collections_action def contextMenuEvent(self, event): self.context_menu.popup(event.globalPos()) event.accept() # }}} # Drag 'n Drop {{{ @classmethod def paths_from_event(cls, event): ''' Accept a drop event and return a list of paths that can be read from and represent files with extensions. ''' md = event.mimeData() if md.hasFormat('text/uri-list') and not \ md.hasFormat('application/calibre+from_library'): urls = [unicode(u.toLocalFile()) for u in md.urls()] return [ u for u in urls if os.path.splitext(u)[1] and os.path.exists(u) ] def drag_icon(self, cover, multiple): cover = cover.scaledToHeight(120, Qt.SmoothTransformation) if multiple: base_width = cover.width() base_height = cover.height() base = QImage(base_width + 21, base_height + 21, QImage.Format_ARGB32_Premultiplied) base.fill(QColor(255, 255, 255, 0).rgba()) p = QPainter(base) rect = QRect(20, 0, base_width, base_height) p.fillRect(rect, QColor('white')) p.drawRect(rect) rect.moveLeft(10) rect.moveTop(10) p.fillRect(rect, QColor('white')) p.drawRect(rect) rect.moveLeft(0) rect.moveTop(20) p.fillRect(rect, QColor('white')) p.save() p.setCompositionMode(p.CompositionMode_SourceAtop) p.drawImage(rect.topLeft(), cover) p.restore() p.drawRect(rect) p.end() cover = base return QPixmap.fromImage(cover) def drag_data(self): m = self.model() db = m.db rows = self.selectionModel().selectedRows() selected = list(map(m.id, rows)) ids = ' '.join(map(str, selected)) md = QMimeData() md.setData('application/calibre+from_library', ids) fmt = prefs['output_format'] def url_for_id(i): try: ans = db.format_path(i, fmt, index_is_id=True) except: ans = None if ans is None: fmts = db.formats(i, index_is_id=True) if fmts: fmts = fmts.split(',') else: fmts = [] for f in fmts: try: ans = db.format_path(i, f, index_is_id=True) except: ans = None if ans is None: ans = db.abspath(i, index_is_id=True) return QUrl.fromLocalFile(ans) md.setUrls([url_for_id(i) for i in selected]) drag = QDrag(self) col = self.selectionModel().currentIndex().column() md.column_name = self.column_map[col] drag.setMimeData(md) cover = self.drag_icon(m.cover(self.currentIndex().row()), len(selected) > 1) drag.setHotSpot(QPoint(-15, -15)) drag.setPixmap(cover) return drag def event_has_mods(self, event=None): mods = event.modifiers() if event is not None else \ QApplication.keyboardModifiers() return mods & Qt.ControlModifier or mods & Qt.ShiftModifier def mousePressEvent(self, event): ep = event.pos() if self.indexAt(ep) in self.selectionModel().selectedIndexes() and \ event.button() == Qt.LeftButton and not self.event_has_mods(): self.drag_start_pos = ep return QTableView.mousePressEvent(self, event) def mouseMoveEvent(self, event): if not self.drag_allowed: return if self.drag_start_pos is None: return QTableView.mouseMoveEvent(self, event) if self.event_has_mods(): self.drag_start_pos = None return if not (event.buttons() & Qt.LeftButton) or \ (event.pos() - self.drag_start_pos).manhattanLength() \ < QApplication.startDragDistance(): return index = self.indexAt(event.pos()) if not index.isValid(): return drag = self.drag_data() drag.exec_(Qt.CopyAction) self.drag_start_pos = None def dragEnterEvent(self, event): if int(event.possibleActions() & Qt.CopyAction) + \ int(event.possibleActions() & Qt.MoveAction) == 0: return paths = self.paths_from_event(event) if paths: event.acceptProposedAction() def dragMoveEvent(self, event): event.acceptProposedAction() def dropEvent(self, event): paths = self.paths_from_event(event) event.setDropAction(Qt.CopyAction) event.accept() self.files_dropped.emit(paths) # }}} @property def column_map(self): return self._model.column_map def refresh_book_details(self): idx = self.currentIndex() if idx.isValid(): self._model.current_changed(idx, idx) def scrollContentsBy(self, dx, dy): # Needed as Qt bug causes headerview to not always update when scrolling QTableView.scrollContentsBy(self, dx, dy) if dy != 0: self.column_header.update() def scroll_to_row(self, row): if row > -1: h = self.horizontalHeader() for i in range(h.count()): if not h.isSectionHidden( i) and h.sectionViewportPosition(i) >= 0: self.scrollTo(self.model().index(row, i), self.PositionAtCenter) break def set_current_row(self, row=0, select=True): if row > -1 and row < self.model().rowCount(QModelIndex()): h = self.horizontalHeader() logical_indices = list(range(h.count())) logical_indices = [ x for x in logical_indices if not h.isSectionHidden(x) ] pairs = [(x, h.visualIndex(x)) for x in logical_indices if h.visualIndex(x) > -1] if not pairs: pairs = [(0, 0)] pairs.sort(cmp=lambda x, y: cmp(x[1], y[1])) i = pairs[0][0] index = self.model().index(row, i) self.setCurrentIndex(index) if select: sm = self.selectionModel() sm.select(index, sm.ClearAndSelect | sm.Rows) def keyPressEvent(self, ev): val = self.horizontalScrollBar().value() ret = super(BooksView, self).keyPressEvent(ev) if ev.isAccepted() and ev.key() in ( Qt.Key_Home, Qt.Key_End) and ev.modifiers() & Qt.ControlModifier: self.horizontalScrollBar().setValue(val) return ret def ids_to_rows(self, ids): row_map = OrderedDict() ids = frozenset(ids) m = self.model() for row in xrange(m.rowCount(QModelIndex())): if len(row_map) >= len(ids): break c = m.id(row) if c in ids: row_map[c] = row return row_map def select_rows(self, identifiers, using_ids=True, change_current=True, scroll=True): ''' Select rows identified by identifiers. identifiers can be a set of ids, row numbers or QModelIndexes. ''' rows = set([x.row() if hasattr(x, 'row') else x for x in identifiers]) if using_ids: rows = set([]) identifiers = set(identifiers) m = self.model() for row in xrange(m.rowCount(QModelIndex())): if m.id(row) in identifiers: rows.add(row) rows = list(sorted(rows)) if rows: row = rows[0] if change_current: self.set_current_row(row, select=False) if scroll: self.scroll_to_row(row) sm = self.selectionModel() sel = QItemSelection() m = self.model() max_col = m.columnCount(QModelIndex()) - 1 # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda (i, x): i - x): group = list(map(operator.itemgetter(1), g)) sel.merge( QItemSelection(m.index(min(group), 0), m.index(max(group), max_col)), sm.Select) sm.select(sel, sm.ClearAndSelect)
def makemenu_helper(widget, menu_spec, menu = None): """ Make and return a reusable or one-time-use (at caller's option) popup menu whose structure is specified by menu_spec, which is a list of menu item specifiers, each of which is either None (for a separator) or a tuple of the form (menu text, callable or submenu, option1, option2, ...) with 0 or more options (described below). A submenu can be either another menu_spec list, or a QMenu object (but in the latter case the menu text is ignored -- maybe it comes from that QMenu object somehow -- not sure if this was different in Qt3). In either case it is the 2nd menu-item-tuple element, in place of the callable. Otherwise the callable must satisfy the python 'callable' predicate, and is executed if the menu item is chosen, wrapped inside another function which handles Undo checkpointing and Undo-command-name setting. The options in a menu item tuple can be zero or more (in any order, duplicates allowed) of the following: 'disabled' -- the menu item should be disabled; 'checked' -- the menu item will be checked; None -- this option is legal but ignored (but the callable must still satisfy the python predicate "callable"; constants.noop might be useful for that case). The Qt3 version also supported tuple-options consisting of one of the words 'iconset' and 'whatsThis' followed by an appropriate argument, but those have not yet been ported to Qt4 (except possibly for disabled menu items -- UNTESTED). Unrecognized options may or may not generate warnings, and are otherwise ignored. [###FIX that -- they always ought to print a warning to developers. Note that right now they do iff 'disabled' is one of the options and ATOM_DEBUG is set.] The 'widget' argument should be the Qt widget which is using this function to put up a menu. If the menu argument is provided, it should be a QMenu to which we'll add items; otherwise we create our own QMenu and add items to it. """ from utilities.debug import print_compact_traceback import types if menu is None: menu = QMenu(widget) ## menu.show() #bruce 070514 removed menu.show() to fix a cosmetic and performance bug # (on Mac, possibly on other platforms too; probably unreported) # in which the debug menu first appears in screen center, slowly grows # to full size while remaining blank, then moves to its final position # and looks normal (causing a visual glitch, and a 2-3 second delay # in being able to use it). May fix similar issues with other menus. # If this causes harm for some menus or platforms, we can adapt it. # bruce 040909-16 moved this method from basicMode to GLPane, # leaving a delegator for it in basicMode. # (bruce was not the original author, but modified it) #menu = QMenu( widget) for m in menu_spec: try: #bruce 050416 added try/except as debug code and for safety menutext = m and widget.trUtf8(m[0]) if m and isinstance(m[1], QMenu): #bruce 041010 added this case submenu = m[1] #menu.insertItem( menutext, submenu ) menu.addMenu(submenu) # how do I get menutext in there? # (similar code might work for QAction case too, not sure) elif m and isinstance(m[1], types.ListType): #bruce 041103 added this case submenu = QMenu(menutext, menu) submenu = makemenu_helper(widget, m[1], submenu) # [this used to call widget.makemenu] menu.addMenu(submenu) elif m: assert callable(m[1]), \ "%r[1] needs to be a callable" % (m,) #bruce 041103 # transform m[1] into a new callable that makes undo checkpoints and provides an undo command-name # [bruce 060324 for possible bugs in undo noticing cmenu items, and for the cmdnames] func = wrap_callable_for_undo(m[1], cmdname = m[0]) # guess about cmdname, but it might be reasonable for A7 as long as we ensure weird characters won't confuse it import foundation.changes as changes changes.keep_forever(func) # THIS IS BAD (memory leak), but it's not a severe one, so ok for A7 [bruce 060324] # (note: the hard part about removing these when we no longer need them is knowing when to do that # if the user ends up not selecting anything from the menu. Also, some callers make these # menus for reuse multiple times, and for them we never want to deallocate func even when some # menu command gets used. We could solve both of these by making the caller pass a place to keep these # which it would deallocate someday or which would ensure only one per distinct kind of menu is kept. #e) if 'disabled' not in m[2:]: act = QAction(widget) act.setText( menutext) if 'checked' in m[2:]: act.setCheckable(True) act.setChecked(True) menu.addAction(act) widget.connect(act, SIGNAL("activated()"), func) else: # disabled case # [why is this case done differently, in this Qt4 port?? -- bruce 070522 question] insert_command_into_menu(menu, menutext, func, options = m[2:], raw_command = True) else: menu.addSeparator() #bruce 070522 bugfix -- before this, separators were placed lower down or dropped # so as not to come before disabled items, for unknown reasons. # (Speculation: maybe because insertSeparator was used, since addSeparator didn't work or wasn't noticed, # and since disabled item were added by an older function (also for unknown reasons)?) pass except Exception, e: if isinstance(e, SystemExit): raise print_compact_traceback("exception in makemenu_helper ignored, for %r:\n" % (m,) ) #bruce 070522 restored this (was skipped in Qt4 port) pass #e could add a fake menu item here as an error message