class ChooseLibraryAction(InterfaceAction): name = 'Choose Library' action_spec = (_('Choose Library'), 'lt.png', _('Choose calibre library to work with'), None) dont_add_to = frozenset(('context-menu-device', )) action_add_menu = True action_menu_clone_qaction = _('Switch/create library...') restore_view_state = pyqtSignal(object) def genesis(self): self.prev_lname = self.last_lname = '' self.count_changed(0) self.action_choose = self.menuless_qaction self.action_exim = ac = QAction(_('Export/import all calibre data'), self.gui) ac.triggered.connect(self.exim_data) self.stats = LibraryUsageStats() self.popup_type = (QToolButton.InstantPopup if len(self.stats.stats) > 1 else QToolButton.MenuButtonPopup) if len(self.stats.stats) > 1: self.action_choose.triggered.connect(self.choose_library) else: self.qaction.triggered.connect(self.choose_library) self.choose_menu = self.qaction.menu() ac = self.create_action(spec=(_('Pick a random book'), 'random.png', None, None), attr='action_pick_random') ac.triggered.connect(self.pick_random) if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): self.choose_menu.addAction(self.action_choose) self.quick_menu = QMenu(_('Quick switch')) self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu) self.rename_menu = QMenu(_('Rename library')) self.rename_menu_action = self.choose_menu.addMenu( self.rename_menu) self.choose_menu.addAction(ac) self.delete_menu = QMenu(_('Remove library')) self.delete_menu_action = self.choose_menu.addMenu( self.delete_menu) self.choose_menu.addAction(self.action_exim) else: self.choose_menu.addAction(ac) self.rename_separator = self.choose_menu.addSeparator() self.switch_actions = [] for i in range(5): ac = self.create_action(spec=('', None, None, None), attr='switch_action%d' % i) ac.setObjectName(unicode_type(i)) self.switch_actions.append(ac) ac.setVisible(False) connect_lambda( ac.triggered, self, lambda self: self.switch_requested(self.qs_locations[int( self.gui.sender().objectName())]), type=Qt.QueuedConnection) self.choose_menu.addAction(ac) self.rename_separator = self.choose_menu.addSeparator() self.maintenance_menu = QMenu(_('Library maintenance')) ac = self.create_action(spec=(_('Library metadata backup status'), 'lt.png', None, None), attr='action_backup_status') ac.triggered.connect(self.backup_status, type=Qt.QueuedConnection) self.maintenance_menu.addAction(ac) ac = self.create_action(spec=(_('Check library'), 'lt.png', None, None), attr='action_check_library') ac.triggered.connect(self.check_library, type=Qt.QueuedConnection) self.maintenance_menu.addAction(ac) ac = self.create_action(spec=(_('Restore database'), 'lt.png', None, None), attr='action_restore_database') ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection) self.maintenance_menu.addAction(ac) self.choose_menu.addMenu(self.maintenance_menu) self.view_state_map = {} self.restore_view_state.connect(self._restore_view_state, type=Qt.QueuedConnection) ac = self.create_action(spec=(_('Switch to previous library'), 'lt.png', None, None), attr='action_previous_library') ac.triggered.connect(self.switch_to_previous_library, type=Qt.QueuedConnection) self.gui.keyboard.register_shortcut(self.unique_name + '-' + 'action_previous_library', ac.text(), action=ac, group=self.action_spec[0], default_keys=('Ctrl+Alt+p', )) self.gui.addAction(ac) @property def preserve_state_on_switch(self): ans = getattr(self, '_preserve_state_on_switch', None) if ans is None: self._preserve_state_on_switch = ans = \ self.gui.library_view.preserve_state(require_selected_ids=False) return ans def pick_random(self, *args): self.gui.iactions['Pick Random Book'].pick_random() def exim_data(self): if isportable: return error_dialog( self.gui, _('Cannot export/import'), _('You are running calibre portable, all calibre data is already in the' ' calibre portable folder. Export/import is unavailable.'), show=True) if self.gui.job_manager.has_jobs(): return error_dialog( self.gui, _('Cannot export/import'), _('Cannot export/import data while there are running jobs.'), show=True) from calibre.gui2.dialogs.exim import EximDialog d = EximDialog(parent=self.gui) if d.exec_() == d.Accepted: if d.restart_needed: self.gui.iactions['Restart'].restart() def library_name(self): db = self.gui.library_view.model().db path = db.library_path if isbytestring(path): path = path.decode(filesystem_encoding) path = path.replace(os.sep, '/') return self.stats.pretty(path) def update_tooltip(self, count): tooltip = self.action_spec[2] + '\n\n' + ngettext( '{0} [{1} book]', '{0} [{1} books]', count).format( getattr(self, 'last_lname', ''), count) a = self.qaction a.setToolTip(tooltip) a.setStatusTip(tooltip) a.setWhatsThis(tooltip) def library_changed(self, db): lname = self.stats.library_used(db) if lname != self.last_lname: self.prev_lname = self.last_lname self.last_lname = lname if len(lname) > 16: lname = lname[:16] + '…' a = self.qaction a.setText(lname.replace( '&', '&&&')) # I have no idea why this requires a triple ampersand self.update_tooltip(db.count()) self.build_menus() state = self.view_state_map.get( self.stats.canonicalize_path(db.library_path), None) if state is not None: self.restore_view_state.emit(state) def _restore_view_state(self, state): self.preserve_state_on_switch.state = state def initialization_complete(self): self.library_changed(self.gui.library_view.model().db) def switch_to_previous_library(self): db = self.gui.library_view.model().db locations = list(self.stats.locations(db)) for name, loc in locations: is_prev_lib = name == self.prev_lname if is_prev_lib: self.switch_requested(loc) break def build_menus(self): if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): return db = self.gui.library_view.model().db locations = list(self.stats.locations(db)) for ac in self.switch_actions: ac.setVisible(False) self.quick_menu.clear() self.rename_menu.clear() self.delete_menu.clear() quick_actions, rename_actions, delete_actions = [], [], [] for name, loc in locations: is_prev_lib = name == self.prev_lname name = name.replace('&', '&&') ac = self.quick_menu.addAction( name, Dispatcher(partial(self.switch_requested, loc))) ac.setStatusTip(_('Switch to: %s') % loc) if is_prev_lib: f = ac.font() f.setBold(True) ac.setFont(f) quick_actions.append(ac) ac = self.rename_menu.addAction( name, Dispatcher(partial(self.rename_requested, name, loc))) rename_actions.append(ac) ac.setStatusTip(_('Rename: %s') % loc) ac = self.delete_menu.addAction( name, Dispatcher(partial(self.delete_requested, name, loc))) delete_actions.append(ac) ac.setStatusTip(_('Remove: %s') % loc) if is_prev_lib: ac.setFont(f) qs_actions = [] locations_by_frequency = locations if len(locations) >= tweaks['many_libraries']: locations_by_frequency = list( self.stats.locations(db, limit=sys.maxsize)) for i, x in enumerate( locations_by_frequency[:len(self.switch_actions)]): name, loc = x name = name.replace('&', '&&') ac = self.switch_actions[i] ac.setText(name) ac.setStatusTip(_('Switch to: %s') % loc) ac.setVisible(True) qs_actions.append(ac) self.qs_locations = [i[1] for i in locations_by_frequency] self.quick_menu_action.setVisible(bool(locations)) self.rename_menu_action.setVisible(bool(locations)) self.delete_menu_action.setVisible(bool(locations)) self.gui.location_manager.set_switch_actions(quick_actions, rename_actions, delete_actions, qs_actions, self.action_choose) # Allow the cloned actions in the OS X global menubar to update for a in (self.qaction, self.menuless_qaction): a.changed.emit() def location_selected(self, loc): enabled = loc == 'library' self.qaction.setEnabled(enabled) self.menuless_qaction.setEnabled(enabled) def rename_requested(self, name, location): LibraryDatabase = db_class() loc = location.replace('/', os.sep) base = os.path.dirname(loc) old_name = name.replace('&&', '&') newname, ok = QInputDialog.getText( self.gui, _('Rename') + ' ' + old_name, '<p>' + _('Choose a new name for the library <b>%s</b>. ') % name + '<p>' + _('Note that the actual library folder will be renamed.'), text=old_name) newname = sanitize_file_name(unicode_type(newname)) if not ok or not newname or newname == old_name: return newloc = os.path.join(base, newname) if os.path.exists(newloc): return error_dialog( self.gui, _('Already exists'), _('The folder %s already exists. Delete it first.') % newloc, show=True) if (iswindows and len(newloc) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT): return error_dialog(self.gui, _('Too long'), _('Path to library too long. Must be less than' ' %d characters.') % LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT, show=True) if not os.path.exists(loc): error_dialog( self.gui, _('Not found'), _('Cannot rename as no library was found at %s. ' 'Try switching to this library first, then switch back ' 'and retry the renaming.') % loc, show=True) return self.gui.library_broker.remove_library(loc) try: os.rename(loc, newloc) except: import traceback det_msg = 'Location: %r New Location: %r\n%s' % ( loc, newloc, traceback.format_exc()) error_dialog( self.gui, _('Rename failed'), _('Failed to rename the library at %s. ' 'The most common cause for this is if one of the files' ' in the library is open in another program.') % loc, det_msg=det_msg, show=True) return self.stats.rename(location, newloc) self.build_menus() self.gui.iactions['Copy To Library'].build_menus() def delete_requested(self, name, location): loc = location.replace('/', os.sep) if not question_dialog( self.gui, _('Library removed'), _('The library %s has been removed from calibre. ' 'The files remain on your computer, if you want ' 'to delete them, you will have to do so manually.') % ('<code>%s</code>' % loc), override_icon='dialog_information.png', yes_text=_('&OK'), no_text=_('&Undo'), yes_icon='ok.png', no_icon='edit-undo.png'): return self.stats.remove(location) self.gui.library_broker.remove_library(location) self.build_menus() self.gui.iactions['Copy To Library'].build_menus() if os.path.exists(loc): open_local_file(loc) def backup_status(self, location): self.__backup_status_dialog = d = BackupStatus(self.gui) d.show() def mark_dirty(self): db = self.gui.library_view.model().db db.dirtied(list(db.data.iterallids())) info_dialog( self.gui, _('Backup metadata'), _('Metadata will be backed up while calibre is running, at the ' 'rate of approximately 1 book every three seconds.'), show=True) def restore_database(self): LibraryDatabase = db_class() m = self.gui.library_view.model() db = m.db if (iswindows and len(db.library_path) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT): return error_dialog( self.gui, _('Too long'), _('Path to library too long. Must be less than' ' %d characters. Move your library to a location with' ' a shorter path using Windows Explorer, then point' ' calibre to the new location and try again.') % LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT, show=True) from calibre.gui2.dialogs.restore_library import restore_database m = self.gui.library_view.model() m.stop_metadata_backup() db = m.db db.prefs.disable_setting = True if restore_database(db, self.gui): self.gui.library_moved(db.library_path) def check_library(self): from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck self.gui.library_view.save_state() m = self.gui.library_view.model() m.stop_metadata_backup() db = m.db db.prefs.disable_setting = True library_path = db.library_path d = DBCheck(self.gui, db) d.start() try: m.close() except: pass d.break_cycles() self.gui.library_moved(library_path) if d.rejected: return if d.error is None: if not question_dialog( self.gui, _('Success'), _('Found no errors in your calibre library database.' ' Do you want calibre to check if the files in your ' ' library match the information in the database?')): return else: return error_dialog( self.gui, _('Failed'), _('Database integrity check failed, click Show details' ' for details.'), show=True, det_msg=d.error[1]) self.gui.status_bar.show_message( _('Starting library scan, this may take a while')) try: QCoreApplication.processEvents() d = CheckLibraryDialog(self.gui, m.db) if not d.do_exec(): info_dialog( self.gui, _('No problems found'), _('The files in your library match the information ' 'in the database.'), show=True) finally: self.gui.status_bar.clear_message() def look_for_portable_lib(self, db, location): base = get_portable_base() if base is None: return False, None loc = location.replace('/', os.sep) candidate = os.path.join(base, os.path.basename(loc)) if db.exists_at(candidate): newloc = candidate.replace(os.sep, '/') self.stats.rename(location, newloc) return True, newloc return False, None def switch_requested(self, location): if not self.change_library_allowed(): return db = self.gui.library_view.model().db current_lib = self.stats.canonicalize_path(db.library_path) self.view_state_map[current_lib] = self.preserve_state_on_switch.state loc = location.replace('/', os.sep) exists = db.exists_at(loc) if not exists: exists, new_location = self.look_for_portable_lib(db, location) if exists: location = new_location loc = location.replace('/', os.sep) if not exists: d = MovedDialog(self.stats, location, self.gui) ret = d.exec_() self.build_menus() self.gui.iactions['Copy To Library'].build_menus() if ret == d.Accepted: loc = d.newloc.replace('/', os.sep) else: return # from calibre.utils.mem import memory # import weakref # from PyQt5.Qt import QTimer # self.dbref = weakref.ref(self.gui.library_view.model().db) # self.before_mem = memory() self.gui.library_moved(loc, allow_rebuild=True) # QTimer.singleShot(5000, self.debug_leak) def debug_leak(self): import gc from calibre.utils.mem import memory ref = self.dbref for i in range(3): gc.collect() if ref() is not None: print('DB object alive:', ref()) for r in gc.get_referrers(ref())[:10]: print(r) print() print('before:', self.before_mem) print('after:', memory()) print() self.dbref = self.before_mem = None def count_changed(self, new_count): self.update_tooltip(new_count) def choose_library(self, *args): if not self.change_library_allowed(): return from calibre.gui2.dialogs.choose_library import ChooseLibrary self.gui.library_view.save_state() db = self.gui.library_view.model().db location = self.stats.canonicalize_path(db.library_path) self.pre_choose_dialog_location = location c = ChooseLibrary(db, self.choose_library_callback, self.gui) c.exec_() def choose_library_callback(self, newloc, copy_structure=False, library_renamed=False): self.gui.library_moved(newloc, copy_structure=copy_structure, allow_rebuild=True) if library_renamed: self.stats.rename(self.pre_choose_dialog_location, prefs['library_path']) self.build_menus() self.gui.iactions['Copy To Library'].build_menus() def change_library_allowed(self): if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): warning_dialog( self.gui, _('Not allowed'), _('You cannot change libraries while using the environment' ' variable CALIBRE_OVERRIDE_DATABASE_PATH.'), show=True) return False if self.gui.job_manager.has_jobs(): warning_dialog(self.gui, _('Not allowed'), _('You cannot change libraries while jobs' ' are running.'), show=True) return False if self.gui.proceed_question.questions: warning_dialog(self.gui, _('Not allowed'), _('You cannot change libraries until all' ' updates are accepted or rejected.'), show=True) return False return True
class ChooseLibraryAction(InterfaceAction): name = 'Choose Library' action_spec = (_('Choose Library'), 'lt.png', _('Choose calibre library to work with'), None) dont_add_to = frozenset(['context-menu-device']) action_add_menu = True action_menu_clone_qaction = _('Switch/create library...') restore_view_state = pyqtSignal(object) def genesis(self): self.count_changed(0) self.action_choose = self.menuless_qaction self.stats = LibraryUsageStats() self.popup_type = (QToolButton.InstantPopup if len(self.stats.stats) > 1 else QToolButton.MenuButtonPopup) if len(self.stats.stats) > 1: self.action_choose.triggered.connect(self.choose_library) else: self.qaction.triggered.connect(self.choose_library) self.choose_menu = self.qaction.menu() ac = self.create_action(spec=(_('Pick a random book'), 'random.png', None, None), attr='action_pick_random') ac.triggered.connect(self.pick_random) if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): self.choose_menu.addAction(self.action_choose) self.quick_menu = QMenu(_('Quick switch')) self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu) self.rename_menu = QMenu(_('Rename library')) self.rename_menu_action = self.choose_menu.addMenu(self.rename_menu) self.choose_menu.addAction(ac) self.delete_menu = QMenu(_('Remove library')) self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu) else: self.choose_menu.addAction(ac) self.rename_separator = self.choose_menu.addSeparator() self.switch_actions = [] for i in range(5): ac = self.create_action(spec=('', None, None, None), attr='switch_action%d'%i) self.switch_actions.append(ac) ac.setVisible(False) ac.triggered.connect(partial(self.qs_requested, i), type=Qt.QueuedConnection) self.choose_menu.addAction(ac) self.rename_separator = self.choose_menu.addSeparator() self.maintenance_menu = QMenu(_('Library Maintenance')) ac = self.create_action(spec=(_('Library metadata backup status'), 'lt.png', None, None), attr='action_backup_status') ac.triggered.connect(self.backup_status, type=Qt.QueuedConnection) self.maintenance_menu.addAction(ac) ac = self.create_action(spec=(_('Check library'), 'lt.png', None, None), attr='action_check_library') ac.triggered.connect(self.check_library, type=Qt.QueuedConnection) self.maintenance_menu.addAction(ac) ac = self.create_action(spec=(_('Restore database'), 'lt.png', None, None), attr='action_restore_database') ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection) self.maintenance_menu.addAction(ac) self.choose_menu.addMenu(self.maintenance_menu) self.view_state_map = {} self.restore_view_state.connect(self._restore_view_state, type=Qt.QueuedConnection) @property def preserve_state_on_switch(self): ans = getattr(self, '_preserve_state_on_switch', None) if ans is None: self._preserve_state_on_switch = ans = \ self.gui.library_view.preserve_state(require_selected_ids=False) return ans def pick_random(self, *args): self.gui.iactions['Pick Random Book'].pick_random() def library_name(self): db = self.gui.library_view.model().db path = db.library_path if isbytestring(path): path = path.decode(filesystem_encoding) path = path.replace(os.sep, '/') return self.stats.pretty(path) def update_tooltip(self, count): tooltip = self.action_spec[2] + '\n\n' + _('{0} [{1} books]').format( getattr(self, 'last_lname', ''), count) a = self.qaction a.setToolTip(tooltip) a.setStatusTip(tooltip) a.setWhatsThis(tooltip) def library_changed(self, db): lname = self.stats.library_used(db) self.last_lname = lname if len(lname) > 16: lname = lname[:16] + u'…' a = self.qaction a.setText(lname) self.update_tooltip(db.count()) self.build_menus() state = self.view_state_map.get(self.stats.canonicalize_path( db.library_path), None) if state is not None: self.restore_view_state.emit(state) def _restore_view_state(self, state): self.preserve_state_on_switch.state = state def initialization_complete(self): self.library_changed(self.gui.library_view.model().db) def build_menus(self): if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): return db = self.gui.library_view.model().db locations = list(self.stats.locations(db)) for ac in self.switch_actions: ac.setVisible(False) self.quick_menu.clear() self.qs_locations = [i[1] for i in locations] self.rename_menu.clear() self.delete_menu.clear() quick_actions, rename_actions, delete_actions = [], [], [] for name, loc in locations: ac = self.quick_menu.addAction(name, Dispatcher(partial(self.switch_requested, loc))) ac.setStatusTip(_('Switch to: %s') % loc) quick_actions.append(ac) ac = self.rename_menu.addAction(name, Dispatcher(partial(self.rename_requested, name, loc))) rename_actions.append(ac) ac.setStatusTip(_('Rename: %s') % loc) ac = self.delete_menu.addAction(name, Dispatcher(partial(self.delete_requested, name, loc))) delete_actions.append(ac) ac.setStatusTip(_('Remove: %s') % loc) qs_actions = [] for i, x in enumerate(locations[:len(self.switch_actions)]): name, loc = x ac = self.switch_actions[i] ac.setText(name) ac.setStatusTip(_('Switch to: %s') % loc) ac.setVisible(True) qs_actions.append(ac) self.quick_menu_action.setVisible(bool(locations)) self.rename_menu_action.setVisible(bool(locations)) self.delete_menu_action.setVisible(bool(locations)) self.gui.location_manager.set_switch_actions(quick_actions, rename_actions, delete_actions, qs_actions, self.action_choose) # Allow the cloned actions in the OS X global menubar to update for a in (self.qaction, self.menuless_qaction): a.changed.emit() def location_selected(self, loc): enabled = loc == 'library' self.qaction.setEnabled(enabled) def rename_requested(self, name, location): LibraryDatabase = db_class() loc = location.replace('/', os.sep) base = os.path.dirname(loc) newname, ok = QInputDialog.getText(self.gui, _('Rename') + ' ' + name, '<p>'+_('Choose a new name for the library <b>%s</b>. ')%name + '<p>'+_('Note that the actual library folder will be renamed.'), text=name) newname = sanitize_file_name_unicode(unicode(newname)) if not ok or not newname or newname == name: return newloc = os.path.join(base, newname) if os.path.exists(newloc): return error_dialog(self.gui, _('Already exists'), _('The folder %s already exists. Delete it first.') % newloc, show=True) if (iswindows and len(newloc) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT): return error_dialog(self.gui, _('Too long'), _('Path to library too long. Must be less than' ' %d characters.')%LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT, show=True) if not os.path.exists(loc): error_dialog(self.gui, _('Not found'), _('Cannot rename as no library was found at %s. ' 'Try switching to this library first, then switch back ' 'and retry the renaming.')%loc, show=True) return try: os.rename(loc, newloc) except: import traceback det_msg = 'Location: %r New Location: %r\n%s'%(loc, newloc, traceback.format_exc()) error_dialog(self.gui, _('Rename failed'), _('Failed to rename the library at %s. ' 'The most common cause for this is if one of the files' ' in the library is open in another program.') % loc, det_msg=det_msg, show=True) return self.stats.rename(location, newloc) self.build_menus() self.gui.iactions['Copy To Library'].build_menus() def delete_requested(self, name, location): loc = location.replace('/', os.sep) if not question_dialog( self.gui, _('Library removed'), _( 'The library %s has been removed from calibre. ' 'The files remain on your computer, if you want ' 'to delete them, you will have to do so manually.') % ('<code>%s</code>' % loc), override_icon='dialog_information.png', yes_text=_('&OK'), no_text=_('&Undo'), yes_icon='ok.png', no_icon='edit-undo.png'): return self.stats.remove(location) self.build_menus() self.gui.iactions['Copy To Library'].build_menus() if os.path.exists(loc): open_local_file(loc) def backup_status(self, location): self.__backup_status_dialog = d = BackupStatus(self.gui) d.show() def mark_dirty(self): db = self.gui.library_view.model().db db.dirtied(list(db.data.iterallids())) info_dialog(self.gui, _('Backup metadata'), _('Metadata will be backed up while calibre is running, at the ' 'rate of approximately 1 book every three seconds.'), show=True) def restore_database(self): LibraryDatabase = db_class() m = self.gui.library_view.model() db = m.db if (iswindows and len(db.library_path) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT): return error_dialog(self.gui, _('Too long'), _('Path to library too long. Must be less than' ' %d characters. Move your library to a location with' ' a shorter path using Windows Explorer, then point' ' calibre to the new location and try again.')% LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT, show=True) from calibre.gui2.dialogs.restore_library import restore_database m = self.gui.library_view.model() m.stop_metadata_backup() db = m.db db.prefs.disable_setting = True if restore_database(db, self.gui): self.gui.library_moved(db.library_path, call_close=False) def check_library(self): from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck self.gui.library_view.save_state() m = self.gui.library_view.model() m.stop_metadata_backup() db = m.db db.prefs.disable_setting = True d = DBCheck(self.gui, db) d.start() try: d.conn.close() except: pass d.break_cycles() self.gui.library_moved(db.library_path, call_close=not d.closed_orig_conn) if d.rejected: return if d.error is None: if not question_dialog(self.gui, _('Success'), _('Found no errors in your calibre library database.' ' Do you want calibre to check if the files in your ' ' library match the information in the database?')): return else: return error_dialog(self.gui, _('Failed'), _('Database integrity check failed, click Show details' ' for details.'), show=True, det_msg=d.error[1]) self.gui.status_bar.show_message( _('Starting library scan, this may take a while')) try: QCoreApplication.processEvents() d = CheckLibraryDialog(self.gui, m.db) if not d.do_exec(): info_dialog(self.gui, _('No problems found'), _('The files in your library match the information ' 'in the database.'), show=True) finally: self.gui.status_bar.clear_message() def look_for_portable_lib(self, db, location): base = get_portable_base() if base is None: return False, None loc = location.replace('/', os.sep) candidate = os.path.join(base, os.path.basename(loc)) if db.exists_at(candidate): newloc = candidate.replace(os.sep, '/') self.stats.rename(location, newloc) return True, newloc return False, None def switch_requested(self, location): if not self.change_library_allowed(): return db = self.gui.library_view.model().db current_lib = self.stats.canonicalize_path(db.library_path) self.view_state_map[current_lib] = self.preserve_state_on_switch.state loc = location.replace('/', os.sep) exists = db.exists_at(loc) if not exists: exists, new_location = self.look_for_portable_lib(db, location) if exists: location = new_location loc = location.replace('/', os.sep) if not exists: d = MovedDialog(self.stats, location, self.gui) ret = d.exec_() self.build_menus() self.gui.iactions['Copy To Library'].build_menus() if ret == d.Accepted: loc = d.newloc.replace('/', os.sep) else: return # from calibre.utils.mem import memory # import weakref # from PyQt5.Qt import QTimer # self.dbref = weakref.ref(self.gui.library_view.model().db) # self.before_mem = memory() self.gui.library_moved(loc, allow_rebuild=True) # QTimer.singleShot(5000, self.debug_leak) def debug_leak(self): import gc from calibre.utils.mem import memory ref = self.dbref for i in xrange(3): gc.collect() if ref() is not None: print 'DB object alive:', ref() for r in gc.get_referrers(ref())[:10]: print r print print 'before:', self.before_mem print 'after:', memory() print self.dbref = self.before_mem = None def qs_requested(self, idx, *args): self.switch_requested(self.qs_locations[idx]) def count_changed(self, new_count): self.update_tooltip(new_count) def choose_library(self, *args): if not self.change_library_allowed(): return from calibre.gui2.dialogs.choose_library import ChooseLibrary self.gui.library_view.save_state() db = self.gui.library_view.model().db location = self.stats.canonicalize_path(db.library_path) self.pre_choose_dialog_location = location c = ChooseLibrary(db, self.choose_library_callback, self.gui) c.exec_() self.choose_dialog_library_renamed = getattr(c, 'library_renamed', False) def choose_library_callback(self, newloc, copy_structure=False): self.gui.library_moved(newloc, copy_structure=copy_structure, allow_rebuild=True) if getattr(self, 'choose_dialog_library_renamed', False): self.stats.rename(self.pre_choose_dialog_location, prefs['library_path']) self.build_menus() self.gui.iactions['Copy To Library'].build_menus() def change_library_allowed(self): if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): warning_dialog(self.gui, _('Not allowed'), _('You cannot change libraries while using the environment' ' variable CALIBRE_OVERRIDE_DATABASE_PATH.'), show=True) return False if self.gui.job_manager.has_jobs(): warning_dialog(self.gui, _('Not allowed'), _('You cannot change libraries while jobs' ' are running.'), show=True) return False return True
class RulesDialog(Dialog): def __init__(self, parent=None): self.loaded_ruleset = None Dialog.__init__(self, _('Edit tag mapper rules'), 'edit-tag-mapper-rules', parent=parent) def setup_ui(self): self.l = l = QVBoxLayout(self) self.edit_widget = w = Rules(self) l.addWidget(w) l.addWidget(self.bb) self.save_button = b = self.bb.addButton(_('&Save'), self.bb.ActionRole) b.setToolTip(_('Save this ruleset for later re-use')) b.clicked.connect(self.save_ruleset) self.load_button = b = self.bb.addButton(_('&Load'), self.bb.ActionRole) b.setToolTip(_('Load a previously saved ruleset')) self.load_menu = QMenu(self) b.setMenu(self.load_menu) self.build_load_menu() self.test_button = b = self.bb.addButton(_('&Test rules'), self.bb.ActionRole) b.clicked.connect(self.test_rules) @property def rules(self): return self.edit_widget.rules @rules.setter def rules(self, rules): self.edit_widget.rules = rules def save_ruleset(self): if not self.rules: error_dialog(self, _('No rules'), _( 'Cannot save as no rules have been created'), show=True) return text, ok = QInputDialog.getText(self, _('Save ruleset as'), _( 'Enter a name for this ruleset:'), text=self.loaded_ruleset or '') if ok and text: if self.loaded_ruleset and text == self.loaded_ruleset: if not question_dialog(self, _('Are you sure?'), _( 'A ruleset with the name "%s" already exists, do you want to replace it?') % text): return self.loaded_ruleset = text rules = self.rules if rules: tag_maps[text] = self.rules elif text in tag_maps: del tag_maps[text] self.build_load_menu() def build_load_menu(self): self.load_menu.clear() if len(tag_maps): for name, rules in tag_maps.iteritems(): self.load_menu.addAction(name).triggered.connect(partial(self.load_ruleset, name)) self.load_menu.addSeparator() m = self.load_menu.addMenu(_('Delete saved rulesets')) for name, rules in tag_maps.iteritems(): m.addAction(name).triggered.connect(partial(self.delete_ruleset, name)) else: self.load_menu.addAction(_('No saved rulesets available')) def load_ruleset(self, name): self.rules = tag_maps[name] self.loaded_ruleset = name def delete_ruleset(self, name): del tag_maps[name] self.build_load_menu() def test_rules(self): Tester(self.rules, self).exec_()
class EmbedComicMetadata(InterfaceAction): name = 'Embed Comic Metadata' # Declare the main action associated with this plugin if prefs["main_import"]: action_spec = (_L['Import Comic Metadata'], None, _L['Imports the metadata from the comic to calibre'], None) else: action_spec = (_L['Embed Comic Metadata'], None, _L['Embeds calibres metadata into the comic'], None) def genesis(self): # menu self.menu = QMenu(self.gui) # Set the icon for this interface action icon = get_icons('images/icon.png') # need to import this? # The qaction is automatically created from the action_spec defined # above self.qaction.setMenu(self.menu) self.qaction.setIcon(icon) self.qaction.triggered.connect(self.main_menu_triggered) # build menu self.menu.clear() self.build_menu() self.toggle_menu_items() def build_menu(self): for item in config[CONFIG_MENU]["UI_Action_Items"]: if item[CONFIG_NAME] == "seperator": self.menu.addSeparator() continue elif item[CONFIG_TRIGGER_ARG]: triggerfunc = partial(item[CONFIG_TRIGGER_FUNC], self, item[CONFIG_TRIGGER_ARG]) else: triggerfunc = partial(item[CONFIG_TRIGGER_FUNC], self) self.menu_action(item[CONFIG_NAME], item[CONFIG_DESCRIPTION], triggerfunc) # add configuration entry self.menu_action("configure", _L["Configure"], partial(self.interface_action_base_plugin.do_user_config, (self.gui))) def toggle_menu_items(self): for item in config[CONFIG_MENU]["Items"]: action = getattr(self, item[CONFIG_NAME]) action.setVisible(prefs[item[CONFIG_NAME]]) def main_menu_triggered(self): from calibre_plugins.EmbedComicMetadata.main import embed_into_comic, import_to_calibre # Check the preferences for what should be done if (prefs['read_cbi'] and prefs['read_cix']) or (prefs['cbi_embed'] and prefs['cix_embed']): action = "both" elif (prefs['read_cbi']) or (prefs['cbi_embed']): action = "cbi" elif (prefs['read_cix']) or (prefs['cix_embed']): action = "cix" else: return error_dialog(self.gui, _L['Cannot update metadata'], _L['No embed format selected'], show=True) if prefs["main_import"]: import_to_calibre(self, action) else: embed_into_comic(self, action) def apply_settings(self): from calibre_plugins.EmbedComicMetadata.config import prefs # In an actual non trivial plugin, you would probably need to # do something based on the settings in prefs prefs def menu_action(self, name, title, triggerfunc): action = self.create_menu_action(self.menu, name, title, icon=None, shortcut=None, description=None, triggered=triggerfunc, shortcut_name=None) setattr(self, name, action)
class ExecMacroAction(InterfaceAction): name = 'Exec Macro' # Create our top-level menu/toolbar action (text, icon_path, tooltip, keyboard shortcut) action_spec = ('Exec Macro', None, 'Execute current selected macro.', ()) action_type = 'current' def genesis(self): # This method is called once per plugin, do initial setup here self.menu = QMenu(self.gui) self.actions = {} self.rebuild_menu() self.qaction.setMenu(self.menu) self.qaction.setIcon(get_icons('images/icon.png')) self.qaction.triggered.connect(self.execute_current_macro) def rebuild_menu(self): self.menu.clear() self.actions.clear() macros = prefs['macros'] for name in sorted(macros): action = self.create_menu_action_unique( self.menu, name, tooltip=macros[name]['documentation'], image='dot_red.png', shortcut=None, shortcut_name=None, triggered=partial(self.execute_macro, name)) self.actions[name] = action self.mark_current_macro() self.menu.addSeparator() self.create_menu_action_unique(self.menu, _('Manage macros'), tooltip=None, image='config.png', shortcut=None, shortcut_name=None, triggered=partial( show_config_dialog, self)) self.delete_orphan_shortcuts() def delete_orphan_shortcuts(self): prefix = menu_action_unique_name(self, '') to_del = { sc for sc in self.gui.keyboard.shortcuts if sc.startswith(prefix) } new_sc = {menu_action_unique_name(self, name) for name in self.actions} new_sc.add(menu_action_unique_name(self, _('Manage macros'))) to_del -= new_sc for sc in to_del: self.gui.keyboard.unregister_shortcut(sc) self.gui.keyboard.finalize() def mark_current_macro(self): for name, action in self.actions.iteritems(): action.setIconVisibleInMenu(name == prefs['current_macro']) def execute_current_macro(self): self.execute_macro(prefs['current_macro']) def execute_macro(self, name): if name != prefs['current_macro']: prefs['current_macro'] = name self.mark_current_macro() macro = prefs['macros'].get(name, None) if not macro: return log = GUILog() log.outputs.append(ANSIStream()) try: if macro.get('execfromfile'): with open(macro['macrofile'], 'rU') as file: self.execute(file, log) elif macro['program']: program = macro['program'] encoding = self.get_encoding(program) if encoding: program = program.encode(encoding) self.execute(program, log) except: log.exception(_('Failed to execute macro')) error_dialog( self.gui, _('Failed to execute macro'), _('Failed to execute macro, click "Show details" for more information.' ), det_msg=log.plain_text, show=True) def execute(self, program, log): # exec(program, globals().copy(), locals().copy()) vars = globals().copy() vars['self'] = self vars['log'] = log exec(program, vars) def create_menu_action_unique(self, parent_menu, menu_text, image=None, tooltip=None, shortcut=None, triggered=None, is_checked=None, shortcut_name=None, unique_name=None): ''' Create a menu action with the specified criteria and action, using the new InterfaceAction.create_menu_action() function which ensures that regardless of whether a shortcut is specified it will appear in Preferences->Keyboard ''' orig_shortcut = shortcut kb = self.gui.keyboard if unique_name is None: unique_name = menu_text if not shortcut == False: full_unique_name = menu_action_unique_name(self, unique_name) if full_unique_name in kb.shortcuts: shortcut = False else: if shortcut is not None and not shortcut == False: if len(shortcut) == 0: shortcut = None else: shortcut = _(shortcut) if shortcut_name is None: shortcut_name = menu_text.replace('&', '') ac = self.create_menu_action(parent_menu, unique_name, menu_text, icon=None, shortcut=shortcut, description=tooltip, triggered=triggered, shortcut_name=shortcut_name) if shortcut == False and not orig_shortcut == False: if ac.calibre_shortcut_unique_name in self.gui.keyboard.shortcuts: kb.replace_action(ac.calibre_shortcut_unique_name, ac) if image: ac.setIcon(QIcon(I(image))) if is_checked is not None: ac.setCheckable(True) if is_checked: ac.setChecked(True) return ac def get_encoding(self, txt): decl_re = re.compile(r'^[ \t\f]*#.*coding[:=][ \t]*([-\w.]+)') blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)') lines = txt.splitlines() if len(lines) < 1: return None match = decl_re.match(lines[0]) if match: return match.group(1) if len(lines) < 2 or (not blank_re.match(lines[0])): return None match = decl_re.match(lines[1]) if match: return match.group(1) return None
class ImageManager(QWidget): """An ImageManager manages a stack of images (and associated ImageControllers)""" showErrorMessage = pyqtSignal(str, int) imagesChanged = pyqtSignal() imageRaised = pyqtSignal(FITSImagePlotItem) imagePlotRaised = pyqtSignal() def __init__(self, *args): QWidget.__init__(self, *args) self.mainwin = None # 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._label_color = None self._label_bg_brush = 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) self._qa_plot_all.toggled[bool].connect(self._displayAllImages) self._closing = False self._qa_load_clipboard = None self._clipboard_mode = QClipboard.Clipboard QApplication.clipboard().changed[QClipboard.Mode].connect( self._checkClipboardPath) # populate the menu self._repopulateMenu() self.signalShowMessage = None self.signalShowErrorMessage = None def close(self): dprint(1, "closing Manager") self._closing = True for ic in self._imagecons: ic.close() def setShowMessageSignal(self, _signal): self.signalShowMessage = _signal def setShowErrorMessageSignal(self, _signal): self.signalShowErrorMessage = _signal def setMainWindow(self, _mainwin): self.mainwin = _mainwin 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) dialog.filesSelected['QStringList'].connect(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.signalShowErrorMessage.emit( """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.signalShowMessage.emit("""Reading FITS image %s""" % filename, 3000) QApplication.flush() try: image = SkyImage.FITSImagePlotItem(str(filename)) except KeyboardInterrupt: raise except: busy.reset_cursor() traceback.print_exc() print( """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].""" % (filename, str(sys.exc_info()[1]))) self.signalShowErrorMessage.emit( """<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) print("""Loaded FITS image %s""" % filename) self.signalShowMessage.emit("""Loaded FITS image %s""" % filename, 3000) busy.reset_cursor() return ic 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, curry=False): """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, foo=None): """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)) # remove dockable widget imagecon.removeDockWidget() # 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.imagesChanged.emit() if self._imagecons: self.raiseImage(self._imagecons[0]) else: # remove all dock widgets widget_list = self.mainwin.findChildren(QDockWidget) for widget in widget_list: self.mainwin.removeDockWidget(widget) widget.bind_widget.setVisible(False) widget.close() if self.mainwin._current_layout is not self.mainwin.LayoutImageModel: self.mainwin.skyplot.setVisible(False) # reset size to be minus dockables - workaround for bug #164 # self.mainwin.setMaximumWidth(self.mainwin.width() - 700) 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 or emit is None: # added this check as curry() call to this method via signal can be emit=None. self.imagesChanged.emit() def raiseImage(self, imagecon, foo=None): busy = None # 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 fixed with assumption that this signal is now correct according to the old version # self.imageRaised.emit(self._imagecons[0]) # This was the old signal self.imagePlotRaised.emit() 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 signals self.imageRaised.emit(img) # if dockable control dialog is docked and tabbed then raise to front if imagecon._dockable_colour_ctrl is not None: if imagecon._dockable_colour_ctrl.isVisible(): if not imagecon._dockable_colour_ctrl.isFloating(): list_of_tabbed_widgets = self.mainwin.tabifiedDockWidgets( imagecon._dockable_colour_ctrl) if list_of_tabbed_widgets: imagecon._dockable_colour_ctrl.raise_() if busy is not None: busy.reset_cursor() 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() busy.reset_cursor() 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.signalShowErrorMessage.emit( """Error parsing expression "%s": %s.""" % (expression, str(exc))) return None # try to evaluate expression self.signalShowMessage.emit("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.reset_cursor() traceback.print_exc() self.signalShowErrorMessage.emit("""Error evaluating "%s": %s.""" % (expression, str(exc))) return None busy.reset_cursor() if type(result) != numpy.ma.masked_array and type( result) != numpy.ndarray: self.signalShowErrorMessage.emit( """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.signalShowErrorMessage.emit( """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.signalShowErrorMessage.emit( """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.signalShowMessage.emit("""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.reset_cursor() traceback.print_exc() self.signalShowErrorMessage.emit( """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.signalShowMessage.emit("Created new image for %s" % expression, 3000) dprint(2, "image created") busy.reset_cursor() def _createImageController(self, image, name, basename, model=False, save=False): dprint(2, "creating ImageController for", name) ic = ImageController(image, self, self, name, save=save) # attach appropriate signals ic.imageSignalRepaint.connect(self.replot) ic.imageSignalSlice.connect(self.fastReplot) image.connectPlotRiased(self.imagePlotRaised) ic.imageSignalRaise.connect(self._currier.curry(self.raiseImage, ic)) ic.imageSignalUnload.connect(self._currier.curry(self.unloadImage, ic)) ic.imageSignalCenter.connect(self._currier.curry(self.centerImage, ic)) 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) ic.renderControl().displayRangeChanged.connect( self._currier.curry(self._updateDisplayRange, ic.renderControl())) ic.renderControl().displayRangeLocked.connect( 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.imagesChanged.emit() return ic
class BookLayout (QWidget, Ui_BookLayout): def __init__ (self, parent=None): super(BookLayout, self).__init__(parent) self.setupUi(self) self.next.clicked.connect(self.goNext) self.previous.clicked.connect(self.goPrevious) self.treeView = None self.pages_widget = [] self.model = None def load (self,model=None): if model!=None: self.model = model self.model.print_() if self.treeView != None: self.treeView.disconnect() self.treeView.setParent(None) self.tree_view_layout.removeWidget(self.treeView) self.tree_model.disconnect() self.tree_model.setParent(None) else: self.stackedWidget.removeWidget(self.page_3) self.stackedWidget.removeWidget(self.page_4) for page_widget in self.pages_widget : page_widget.setParent(None) self.stackedWidget.removeWidget(page_widget) self.pages_widget .clear() self.treeView = CustomTreeView(self.tree_view_page) # self.treeView.setDragEnabled(True) # self.treeView.setAcceptDrops(True) self.treeView.setDropIndicatorShown(True) self.treeView.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) self.treeView.setObjectName("treeView") self.tree_view_layout.addWidget(self.treeView) self.treeView.setIndentation(10) self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.treeView.customContextMenuRequested.connect(self.onContextMenu) self.treeView.activated.connect(self.changeCurrentPage) self.tree_model = TreeModel(self.model) self.tree_model.dataChanged.connect(self.updateModel) self.treeView.setModel(self.tree_model) self.treeView.setWindowTitle("Simple Tree Model") self.treeView.header().hide() self.treeView.setAlternatingRowColors(True) self.contextMenu = QMenu(self.treeView) for child in self.model.root.children: self.processChapter(child) print ('nombre de page :',self.stackedWidget.count()) if self.stackedWidget.count() >= 1 : self.next.setEnabled(True) # self.stackedWidget.removeWidget(self.page) # self.stackedWidget.removeWidget(self.page_2) # self.stackedWidget.addWidget(self.book_homepage) self.stackedWidget.setCurrentIndex(0) def reload (self): print("-----------------") self.model.print_() if self.treeView != None: self.treeView.setParent(None) self.tree_view_layout.removeWidget(self.treeView) else: self.stackedWidget.removeWidget(self.page_3) self.stackedWidget.removeWidget(self.page_4) for page_widget in self.pages_widget : page_widget.setParent(None) self.stackedWidget.removeWidget(page_widget) self.pages_widget.clear() self.treeView = CustomTreeView(self.tree_view_page) self.treeView.setDropIndicatorShown(True) self.treeView.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) self.treeView.setObjectName("treeView") self.tree_view_layout.addWidget(self.treeView) self.treeView.setIndentation(10) self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.treeView.customContextMenuRequested.connect(self.onContextMenu) self.treeView.activated.connect(self.changeCurrentPage) self.tree_model = TreeModel(self.model) self.tree_model.dataChanged.connect(self.updateModel) self.treeView.setModel(self.tree_model) self.treeView.setWindowTitle("Simple Tree Model") self.treeView.header().hide() self.treeView.setAlternatingRowColors(True) self.contextMenu = QMenu(self.treeView) # print ('len(self.model.children())', len(self.model.children())) for child in self.model.root.children: self.processChapter(child) # print ('nombre de page :',self.stackedWidget.count()) if self.stackedWidget.count() >= 1 : self.next.setEnabled(True) self.stackedWidget.setCurrentIndex(0) def updateModel(self): print ('change drag and drop') root = self.tree_model.rootItem self.model = Book() self.model.root = Chapitre(None,root.data(0),root.data(1)) for child in root.children: self.processUpdateModel(self.model.root,child) def processUpdateModel(self,parent,node): print ('processUpdateModel') chap = Chapitre(parent,node.data(0),node.data(1)) parent.addChild(chap) for c in node.children: self.processUpdateModel(chap,c) def processChapter (self, chapitre): if len(chapitre.children) != 0: for sub in chapitre.children : self.processChapter(sub) else: if chapitre.content != None : basepath = Config().instance.path_to_book() filename= os.path.join(basepath,chapitre.content) w_page = PageWidget (filename, self) self.stackedWidget.addWidget(w_page) self.pages_widget.append(w_page) chapitre.indice = self.stackedWidget.count() - 1 else: print ('pppppppppp') def onContextMenu(self, point): print ('onCOntextMenu') index = self.treeView.indexAt(point) chapter = self.tree_model.metadata_model(index, QtCore.Qt.EditRole) if index.isValid() : self.contextMenu.clear() action_add = QAction("ajout sous chapitre", self.treeView) action_add.triggered.connect(partial(self.onAddChapter, chapter)) self.contextMenu.addAction(action_add) action_add_page = QAction("ajout Page", self.treeView) action_add_page.triggered.connect(partial(self.onAddPage, chapter)) self.contextMenu.addAction(action_add_page) action_remove = QAction("remove", self.treeView) action_remove.triggered.connect(partial(self.onRemove, chapter)) self.contextMenu.addAction(action_remove) self.contextMenu.exec_(self.treeView.mapToGlobal(point)) def onAddChapter(self, chapter): print ('onAddChapter') if chapter.content != None: chapitre = chapter.getParent() else: chapitre = chapter test = Chapitre(chapitre, "undefined") chapitre.addChild(test) self.load() def onRemove (self, chapter): print ('onRemove') if chapter.parent() != None: chapter.parent.children.remove(chapter) self.load() def onAddPage(self, chapter): print ('onAddPage') filename = QFileDialog.getOpenFileName(self, caption='Choisir le contenu de la page', directory=Config().instance.settings.path_to_book()) if filename : if chapter.content != None: chapitre = chapter.getParent() else: chapitre = chapter print ('chapitre partnt',chapitre.title) test = Chapitre(chapitre, "undefined", os.path.basename(filename[0])) chapitre.addChild(test) self.model.print_() self.reload() def goPrevious (self): print ('goPrevious') new_index = max(0, self.stackedWidget.currentIndex() - 1) self.stackedWidget.setCurrentIndex(new_index) if new_index == 0: self.previous.setEnabled(False) self.next.setEnabled(True) def goNext (self): print ('goNext') new_index = min(self.stackedWidget.count() - 1, self.stackedWidget.currentIndex() + 1) self.stackedWidget.setCurrentIndex(new_index) if new_index == (self.stackedWidget.count() - 1): self.next.setEnabled(False) self.previous.setEnabled(True) def onEdit (self): print ('onEdit') current = None for child in self.model.root.children: if len(child.children) != 0: for sub in child.children : if sub.indice == self.stackedWidget.currentIndex(): current = sub break else: if child.indice == self.stackedWidget.currentIndex(): current = child break if current != None : textEdit = HtmlEditor(self)#BookEditWindow(self.stackedWidget.currentIndex(),current.content, self) textEdit.fileSaved.connect(self.onUpdatePage) textEdit.load(os.path.join(Config().instance.settings.path_to_book(),current.content)) #textEdit.setWindowModality(QtCore.Qt.ApplicationModal) #textEdit.resize(700, 800) textEdit.show() def onUpdatePage (self): print ('onUpdatePage') widget = self.stackedWidget.currentWidget() ind = self.stackedWidget.currentIndex() widget.load() self.stackedWidget.setCurrentIndex(ind) def changeCurrentPage(self, index): print ('changeCurrentPage') indice = self.tree_model.metadata_indice(index, QtCore.Qt.DecorationRole) self.stackedWidget.setCurrentIndex(indice)
class EmbedComicMetadata(InterfaceAction): name = 'Embed Comic Metadata' # Declare the main action associated with this plugin if prefs["main_import"]: action_spec = (_L['Import Comic Metadata'], None, _L['Imports the metadata from the comic to calibre'], None) else: action_spec = (_L['Embed Comic Metadata'], None, _L['Embeds calibres metadata into the comic'], None) def genesis(self): # menu self.menu = QMenu(self.gui) # Get the icon for this interface action icon = self.get_icon('images/embed_comic_metadata.png') # The qaction is automatically created from the action_spec defined # above self.qaction.setMenu(self.menu) self.qaction.setIcon(icon) self.qaction.triggered.connect(self.main_menu_triggered) # build menu self.menu.clear() self.build_menu() self.toggle_menu_items() def build_menu(self): for item in config[CONFIG_MENU]["UI_Action_Items"]: if item[CONFIG_NAME] == "seperator": self.menu.addSeparator() continue elif item[CONFIG_TRIGGER_ARG]: triggerfunc = partial(item[CONFIG_TRIGGER_FUNC], self, item[CONFIG_TRIGGER_ARG]) else: triggerfunc = partial(item[CONFIG_TRIGGER_FUNC], self) self.menu_action(item[CONFIG_NAME], item[CONFIG_DESCRIPTION], triggerfunc) # add configuration entry self.menu_action( "configure", _L["Configure"], partial(self.interface_action_base_plugin.do_user_config, (self.gui))) def toggle_menu_items(self): for item in config[CONFIG_MENU]["Items"]: action = getattr(self, item[CONFIG_NAME]) action.setVisible(prefs[item[CONFIG_NAME]]) def main_menu_triggered(self): from calibre_plugins.EmbedComicMetadata.main import embed_into_comic, import_to_calibre i = prefs["main_import"] # Check the preferences for what should be done if (i and prefs['read_cbi'] and prefs['read_cix']) or (not i and prefs['cbi_embed'] and prefs['cix_embed']): action = "both" elif (i and prefs['read_cbi']) or (not i and prefs['cbi_embed']): action = "cbi" elif (i and prefs['read_cix']) or (not i and prefs['cix_embed']): action = "cix" else: return error_dialog(self.gui, _L['Cannot update metadata'], _L['No embed format selected'], show=True) if i: import_to_calibre(self, action) else: embed_into_comic(self, action) def apply_settings(self): # In an actual non trivial plugin, you would probably need to # do something based on the settings in prefs prefs def menu_action(self, name, title, triggerfunc): action = self.create_menu_action(self.menu, name, title, icon=None, shortcut=None, description=None, triggered=triggerfunc, shortcut_name=None) setattr(self, name, action) def get_icon(self, icon_name): import os from calibre.utils.config import config_dir # Check to see whether the icon exists as a Calibre resource # This will enable skinning if the user stores icons within a folder like: # ...\AppData\Roaming\calibre\resources\images\Plugin Name\ icon_path = os.path.join(config_dir, 'resources', 'images', self.name, icon_name.replace('images/', '')) if os.path.exists(icon_path): pixmap = QPixmap() pixmap.load(icon_path) return QIcon(pixmap) # As we did not find an icon elsewhere, look within our zip resources return get_icons(icon_name)
class MainWindow(QMainWindow): isUpdated = pyqtSignal(bool) hasSkyModel = pyqtSignal(bool) hasSelection = pyqtSignal(bool) modelChanged = pyqtSignal(object) closing = pyqtSignal() signalShowMessage = pyqtSignal([str, int], [str]) signalShowErrorMessage = pyqtSignal([str], [str, int]) ViewModelColumns = [ "name", "RA", "Dec", "type", "Iapp", "I", "Q", "U", "V", "RM", "spi", "shape" ] def __init__(self, parent, max_width=None, max_height=None, hide_on_close=False): QMainWindow.__init__(self, parent) self.signalShowMessage.connect(self.showMessage, type=Qt.QueuedConnection) self.signalShowErrorMessage.connect(self.showErrorMessage, type=Qt.QueuedConnection) 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.setWindowIcon(QIcon(pixmaps.purr_logo.pm())) # central widget setup self.cw = QWidget(self) # The actual min width of the control dialog is ~396 self._ctrl_dialog_min_size = 400 # approx value # The actual min width of the profile/zoom windows is ~256 self._profile_and_zoom_widget_min_size = 300 # approx value # set usable screen space (90% of available) self.max_width = max_width self.max_height = max_height self.setCentralWidget(self.cw) cwlo = QVBoxLayout(self.cw) cwlo.setContentsMargins(5, 5, 5, 5) # make splitter spl1 = self._splitter1 = QSplitter(Qt.Vertical, self.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() self.skyplot.imagesChanged.connect(self._imagesChanged) self.skyplot.setupShowMessages(self.signalShowMessage) self.skyplot.setupShowErrorMessages(self.signalShowErrorMessage) 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) self.hasSkyModel.connect(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.imgman.setMainWindow(self) self.imgman.setShowMessageSignal(self.signalShowMessage) self.imgman.setShowErrorMessageSignal(self.signalShowErrorMessage) self.skyplot.setImageManager(self.imgman) self.imgman.imagesChanged.connect(self._imagesChanged) # 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) self.hasSkyModel.connect(qa_merge.setEnabled) file_menu.addSeparator() qa_save = file_menu.addAction("&Save model", self.saveFile, Qt.CTRL + Qt.Key_S) self.isUpdated.connect(qa_save.setEnabled) qa_save_as = file_menu.addAction("Save model &as...", self.saveFileAs) self.hasSkyModel.connect(qa_save_as.setEnabled) qa_save_selection_as = file_menu.addAction("Save selection as...", self.saveSelectionAs) self.hasSelection.connect(qa_save_selection_as.setEnabled) file_menu.addSeparator() qa_close = file_menu.addAction("&Close model", self.closeFile, Qt.CTRL + Qt.Key_W) self.hasSkyModel.connect(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) self.hasSkyModel.connect(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("U&nselect all", self._unselectAll, Qt.CTRL + Qt.Key_N) 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) self.hasSelection.connect(qa_add_tag.setEnabled) qa_del_tag = em.addAction("&Untag selection...", self.removeTagsFromSelection, Qt.CTRL + Qt.Key_U) self.hasSelection.connect(qa_del_tag.setEnabled) qa_del_sel = em.addAction("&Delete selection", self._deleteSelection) self.hasSelection.connect(qa_del_sel.setEnabled) # Tools menu tm = self._tools_menu = QMenu("&Tools", self) self._qa_tm = menubar.addMenu(tm) self._qa_tm.setVisible(False) self.hasSkyModel.connect(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.isUpdated.emit(False) self.hasSkyModel.emit(False) self.hasSelection.emit(False) self._exiting = False # set initial layout self._current_layout = None self.setLayout(self.LayoutEmpty) dprint(1, "init complete") # layout identifiers LayoutEmpty = "empty" LayoutImage = "image" LayoutImageModel = "model" LayoutSplit = "split" def _getFilenamesFromDropEvent(self, event): """Checks if drop event is valid (i.e. contains a local URL to a FITS file), and returns list of filenames contained therein.""" dprint(1, "drop event:", event.mimeData().text()) if not event.mimeData().hasUrls(): dprint(1, "drop event: no urls") return None filenames = [] for url in event.mimeData().urls(): name = str(url.toLocalFile()) dprint(2, "drop event: name is", name) if name and Images.isFITS(name): filenames.append(name) dprint(2, "drop event: filenames are", filenames) return filenames def dragEnterEvent(self, event): if self._getFilenamesFromDropEvent(event): dprint(1, "drag-enter accepted") event.acceptProposedAction() else: dprint(1, "drag-enter rejected") def dropEvent(self, event): busy = None filenames = self._getFilenamesFromDropEvent(event) dprint(1, "dropping", filenames) if filenames: event.acceptProposedAction() busy = BusyIndicator() for name in filenames: self.imgman.loadImage(name) if busy is not None: busy.reset_cursor() def saveSizes(self): if self._current_layout is not None: dprint(1, "saving sizes for layout", self._current_layout) # save main window size and splitter dimensions sz = self.size() Config.set('%s-main-window-width' % self._current_layout, sz.width()) Config.set('%s-main-window-height' % self._current_layout, sz.height()) for spl, name in ((self._splitter1, "splitter1"), (self._splitter2, "splitter2")): ssz = spl.sizes() for i, sz in enumerate(ssz): Config.set( '%s-%s-size%d' % (self._current_layout, name, i), sz) def loadSizes(self): if self._current_layout is not None: dprint(1, "loading sizes for layout", self._current_layout) # get main window size and splitter dimensions w = Config.getint('%s-main-window-width' % self._current_layout, 0) h = Config.getint('%s-main-window-height' % self._current_layout, 0) dprint(2, "window size is", w, h) if not (w and h): return None self.resize(QSize(w, h)) for spl, name in (self._splitter1, "splitter1"), (self._splitter2, "splitter2"): ssz = [ Config.getint( '%s-%s-size%d' % (self._current_layout, name, i), -1) for i in (0, 1) ] dprint(2, "splitter", name, "sizes", ssz) if all([sz >= 0 for sz in ssz]): spl.setSizes(ssz) else: return None return True def setLayout(self, layout): """Changes the current window layout. Restores sizes etc. from config file.""" if self._current_layout is layout: return dprint(1, "switching to layout", layout) # save sizes to config file self.saveSizes() # remove imgman widget from all layouts for lo in self._skyplot_stack_lo, self._grouptab_stack_lo: if lo.indexOf(self.imgman) >= 0: lo.removeWidget(self.imgman) # assign it to appropriate parent and parent's layout if layout is self.LayoutImage: lo = self._skyplot_stack_lo self.setMaximumSize(self.max_width, self.max_height) self.setBaseSize(self.max_width, self.max_height) size_policy = QSizePolicy() size_policy.setVerticalPolicy(QSizePolicy.Minimum) size_policy.setHorizontalPolicy(QSizePolicy.Expanding) self.setSizePolicy(size_policy) # set central widget size - workaround for bug #164 # self.cw.setFixedSize(self.max_width - self._ctrl_dialog_min_size - self._profile_and_zoom_widget_min_size, self.max_height) # self.cw.setGeometry(0, self.max_width - self._ctrl_dialog_min_size - self._profile_and_zoom_widget_min_size / 2, # self.max_width - self._ctrl_dialog_min_size - self._profile_and_zoom_widget_min_size, self.max_height) elif layout is self.LayoutEmpty: lo = self._skyplot_stack_lo else: lo = self._grouptab_stack_lo self.imgman.setParent(lo.parentWidget()) lo.addWidget(self.imgman, 0) # show/hide panels if layout is self.LayoutEmpty: self.tw.hide() self.grouptab.hide() # self.skyplot.show() elif layout is self.LayoutImage: self.tw.hide() self.grouptab.hide() self.skyplot.show() # setup dockable state from config file if Config.getbool('livezoom-show'): self.skyplot._livezoom.setVisible(True) self.skyplot._dockable_livezoom.setVisible(True) self.addDockWidget(Qt.LeftDockWidgetArea, self.skyplot._dockable_livezoom) if Config.getbool('liveprofile-show'): self.skyplot._liveprofile.setVisible(True) self.skyplot._dockable_liveprofile.setVisible(True) self.addDockWidget(Qt.LeftDockWidgetArea, self.skyplot._dockable_liveprofile) # resize dock areas widget_list = self.findChildren(QDockWidget) size_list = [] result = [] for widget in widget_list: if not isinstance(widget.bind_widget, ImageControlDialog): size_list.append(widget.bind_widget.width()) result.append(widget) dprint(2, f"{widget} width {widget.width()}") dprint( 2, f"{widget} bind_widget width {widget.bind_widget.width()}" ) if isinstance(widget.bind_widget, LiveImageZoom): widget.bind_widget.setMinimumWidth(widget.width()) widget_list = result # resize dock areas self.resizeDocks(widget_list, size_list, Qt.Horizontal) elif layout is self.LayoutImageModel: self.tw.show() self.grouptab.show() self.skyplot.show() # reload sizes self._current_layout = layout if not self.loadSizes(): dprint(1, "no sizes loaded, setting defaults") if layout is self.LayoutEmpty: self.resize(QSize(512, 256)) elif layout is self.LayoutImage: self.resize(QSize(512, 512)) self._splitter2.setSizes([512, 0]) elif layout is self.LayoutImageModel: self.resize(QSize(1024, 512)) self._splitter1.setSizes([256, 256]) self._splitter2.setSizes([256, 256]) def enableUpdates(self, enable=True): """Enables updates of the child widgets. Usually called after startup is completed (i.e. all data loaded)""" self.skyplot.enableUpdates(enable) if enable: if self.model: self.setLayout(self.LayoutImageModel) elif self.imgman.getImages(): self.setLayout(self.LayoutImage) else: self.setLayout(self.LayoutEmpty) self.show() def _showAboutDialog(self): if not self._about_dialog: self._about_dialog = AboutDialog.AboutDialog(self) self._about_dialog.show() def addTool(self, name, callback): """Adds a tool to the Tools menu""" self._tools_menu.addAction( name, self._currier.curry(self._callTool, callback)) def _callTool(self, callback): callback(self, self.model) def _imagesChanged(self): """Called when the set of loaded images has changed""" if self.imgman.getImages(): if self._current_layout is self.LayoutEmpty: self.setLayout(self.LayoutImage) else: if not self.model: self.setLayout(self.LayoutEmpty) def _selectAll(self): if not self.model: return busy = BusyIndicator() for src in self.model.sources: src.selected = True self.model.emitSelection(self) busy.reset_cursor() def _unselectAll(self): if not self.model: return busy = BusyIndicator() for src in self.model.sources: src.selected = False self.model.emitSelection(self) busy.reset_cursor() def _selectInvert(self): if not self.model: return busy = BusyIndicator() for src in self.model.sources: src.selected = not src.selected self.model.emitSelection(self) busy.reset_cursor() def _deleteSelection(self): unselected = [src for src in self.model.sources if not src.selected] nsel = len(self.model.sources) - len(unselected) if QMessageBox.question( self, "Delete selection", """<P>Really deleted %d selected source(s)? %d unselected sources will remain in the model.</P>""" % (nsel, len(unselected)), QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Cancel) != QMessageBox.Ok: return self.model.setSources(unselected) self.signalShowMessage[str].emit("""Deleted %d sources""" % nsel) self.model.emitUpdate(SkyModel.SkyModel.UpdateAll, origin=self) def _showSourceSelector(self): TigGUI.Tools.source_selector.show_source_selector(self, self.model) def _updateModelSelection(self, num, origin=None): """Called when the model selection has been updated.""" self.hasSelection.emit(bool(num)) import Tigger.Models.Formats _formats = [f[1] for f in Tigger.Models.Formats.listFormatsFull()] _load_file_types = [(doc, ["*" + ext for ext in extensions], load) for load, save, doc, extensions in _formats if load] _save_file_types = [(doc, ["*" + ext for ext in extensions], save) for load, save, doc, extensions in _formats if save] def showMessage(self, msg, time=3000): self.statusBar().showMessage(msg, time) def showErrorMessage(self, msg, time=3000): self.qerrmsg.showMessage(msg) def loadImage(self, filename): return self.imgman.loadImage(filename) def setModel(self, model): if model is not None: self.modelChanged.emit(model) if model: self.model = model self.hasSkyModel.emit(True) self.hasSelection.emit(False) self.isUpdated.emit(False) self.model.enableSignals() self.model.connect("updated", self._indicateModelUpdated) self.model.connect("selected", self._updateModelSelection) # pass to children self.tw.setModel(self.model) self.grouptab.setModel(self.model) self.skyplot.setModel(self.model) # add items to View menu self._column_view_menu.clear() self.tw.addColumnViewActionsTo(self._column_view_menu) else: self.model = None self.setWindowTitle("Tigger") self.hasSelection.emit(False) self.isUpdated.emit(False) self.hasSkyModel.emit(False) self.tw.clear() self.grouptab.clear() self.skyplot.setModel(None) def _openFileCallback(self): if not self._open_file_dialog: filters = ";;".join([ "%s (%s)" % (name, " ".join(patterns)) for name, patterns, func in self._load_file_types ]) dialog = self._open_file_dialog = QFileDialog( self, "Open sky model", ".", filters) dialog.setFileMode(QFileDialog.ExistingFile) dialog.setModal(True) dialog.filesSelected['QStringList'].connect(self.openFile) self._open_file_dialog.exec_() return def _mergeFileCallback(self): if not self._merge_file_dialog: filters = ";;".join([ "%s (%s)" % (name, " ".join(patterns)) for name, patterns, func in self._load_file_types ]) dialog = self._merge_file_dialog = QFileDialog( self, "Merge in sky model", ".", filters) dialog.setFileMode(QFileDialog.ExistingFile) dialog.setModal(True) dialog.filesSelected['QStringList'].connect( self._currier.curry(self.openFile, merge=True)) self._merge_file_dialog.exec_() return def openFile(self, _filename=None, _format=None, _merge=False, _show=True): # check that we can close existing model if not _merge and not self._canCloseExistingModel(): return False if isinstance(_filename, QStringList): _filename = _filename[0] _filename = str(_filename) # try to determine the file type filetype, import_func, export_func, doc = Tigger.Models.Formats.resolveFormat( _filename, _format) if import_func is None: self.signalShowErrorMessage.emit( """Error loading model file %s: unknown file format""" % _filename) return # try to load the specified file busy = BusyIndicator() self.signalShowMessage.emit( """Reading %s file %s""" % (filetype, _filename), 3000) QApplication.flush() try: model = import_func(_filename) model.setFilename(_filename) except: busy.reset_cursor() self.signalShowErrorMessage.emit( """Error loading '%s' file %s: %s""" % (filetype, _filename, str(sys.exc_info()[1]))) return else: # set the layout if _show: self.setLayout(self.LayoutImageModel) # add to content if _merge and self.model: self.model.addSources(model.sources) self.signalShowMessage.emit( """Merged in %d sources from '%s' file %s""" % (len(model.sources), filetype, _filename), 3000) self.model.emitUpdate(SkyModel.SkyModel.UpdateAll) else: print("""Loaded %d sources from '%s' file %s""" % (len(model.sources), filetype, _filename)) self.signalShowMessage.emit( """Loaded %d sources from '%s' file %s""" % (len(model.sources), filetype, _filename), 3000) self._display_filename = os.path.basename(_filename) self.setModel(model) self._indicateModelUpdated(updated=False) # only set self.filename if an export function is available for this format. Otherwise set it to None, so that trying to save # the file results in a save-as operation (so that we don't save to a file in an unsupported format). self.filename = _filename if export_func else None finally: busy.reset_cursor() def closeEvent(self, event): dprint(1, "closing") self._exiting = True self.saveSizes() if not self.closeFile(): self._exiting = False event.ignore() return self.skyplot.close() self.imgman.close() self.closing.emit() dprint(1, "invoking os._exit(0)") os._exit(0) QMainWindow.closeEvent(self, event) def _canCloseExistingModel(self): # save model if modified if self.model and self._model_updated: res = QMessageBox.question( self, "Closing sky model", "<P>Model has been modified, would you like to save the changes?</P>", QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save) if res == QMessageBox.Cancel: return False elif res == QMessageBox.Save: if not self.saveFile(confirm=False, overwrite=True): return False # unload model images, unless we are already exiting anyway if not self._exiting: self.imgman.unloadModelImages() return True def closeFile(self): if not self._canCloseExistingModel(): return False # close model self._display_filename = None self.setModel(None) # set the layout self.setLayout(self.LayoutImage if self.imgman.getTopImage() else self. LayoutEmpty) return True def saveFile(self, filename=None, confirm=False, overwrite=True, non_native=False): """Saves file using the specified 'filename'. If filename is None, uses current filename, if that is not set, goes to saveFileAs() to open dialog and get a filename. If overwrite=False, will ask for confirmation before overwriting an existing file. If non_native=False, will ask for confirmation before exporting in non-native format. If confirm=True, will ask for confirmation regardless. Returns True if saving succeeded, False on error (or if cancelled by user). """ if isinstance(filename, QStringList): filename = filename[0] filename = (filename and str(filename)) or self.filename if filename is None: return self.saveFileAs() else: warning = '' # try to determine the file type filetype, import_func, export_func, doc = Tigger.Models.Formats.resolveFormat( filename, None) if export_func is None: self.signalShowErrorMessage.emit( """Error saving model file %s: unsupported output format""" % filename) return if os.path.exists(filename) and not overwrite: warning += "<P>The file already exists and will be overwritten.</P>" if filetype != 'Tigger' and not non_native: warning += """<P>Please note that you are exporting the model using the external format '%s'. Source types, tags and other model features not supported by this format will be omitted during the export.</P>""" % filetype # get confirmation if confirm or warning: dialog = QMessageBox.warning if warning else QMessageBox.question if dialog(self, "Saving sky model", "<P>Save model to %s?</P>%s" % (filename, warning), QMessageBox.Save | QMessageBox.Cancel, QMessageBox.Save) != QMessageBox.Save: return False busy = BusyIndicator() try: export_func(self.model, filename) self.model.setFilename(filename) except: busy.reset_cursor() self.signalShowErrorMessage.emit( """Error saving model file %s: %s""" % (filename, str(sys.exc_info()[1]))) return False else: self.signalShowMessage.emit( """Saved model to file %s""" % filename, 3000) self._display_filename = os.path.basename(filename) self._indicateModelUpdated(updated=False) self.filename = filename return True finally: busy.reset_cursor() def saveFileAs(self, filename=None): """Saves file using the specified 'filename'. If filename is None, opens dialog to get a filename. Returns True if saving succeeded, False on error (or if cancelled by user). """ if filename is None: if not self._save_as_dialog: filters = ";;".join([ "%s (%s)" % (name, " ".join(patterns)) for name, patterns, func in self._save_file_types ]) dialog = self._save_as_dialog = QFileDialog( self, "Save sky model", ".", filters) dialog.setDefaultSuffix(ModelHTML.DefaultExtension) dialog.setFileMode(QFileDialog.AnyFile) dialog.setAcceptMode(QFileDialog.AcceptSave) dialog.setOption(QFileDialog.DontConfirmOverwrite, True) dialog.setModal(True) dialog.filesSelected['QStringList'].connect(self.saveFileAs) return self._save_as_dialog.exec_() == QDialog.Accepted # filename supplied, so save return self.saveFile(filename, confirm=False) def saveSelectionAs(self, filename=None, force=False): if not self.model: return if filename is None: if not self._save_sel_as_dialog: filters = ";;".join([ "%s (%s)" % (name, " ".join(patterns)) for name, patterns, func in self._save_file_types ]) dialog = self._save_sel_as_dialog = QFileDialog( self, "Save sky model", ".", filters) dialog.setDefaultSuffix(ModelHTML.DefaultExtension) dialog.setFileMode(QFileDialog.AnyFile) dialog.setAcceptMode(QFileDialog.AcceptSave) dialog.setOption(QFileDialog.DontConfirmOverwrite, False) dialog.setModal(True) dialog.filesSelected['QStringList'].connect( self.saveSelectionAs) return self._save_sel_as_dialog.exec_() == QDialog.Accepted # save selection if isinstance(filename, QStringList): filename = filename[0] filename = str(filename) selmodel = self.model.copy() sources = [src for src in self.model.sources if src.selected] if not sources: self.signalShowErrorMessage.emit( """You have not selected any sources to save.""") return # try to determine the file type filetype, import_func, export_func, doc = Tigger.Models.Formats.resolveFormat( filename, None) if export_func is None: self.signalShowErrorMessage.emit( """Error saving model file %s: unsupported output format""" % filename) return busy = BusyIndicator() try: export_func(self.model, filename, sources=sources) except: busy.reset_cursor() self.signalShowErrorMessage.emit( """Error saving selection to model file %s: %s""" % (filename, str(sys.exc_info()[1]))) return False else: self.signalShowMessage.emit( """Wrote %d selected source%s to file %s""" % (len(selmodel.sources), "" if len(selmodel.sources) == 1 else "s", filename), 3000) finally: busy.reset_cursor() pass def addTagToSelection(self): if not hasattr(self, '_add_tag_dialog'): self._add_tag_dialog = Widgets.AddTagDialog(self, modal=True) self._add_tag_dialog.setTags(self.model.tagnames) self._add_tag_dialog.setValue(True) if self._add_tag_dialog.exec_() != QDialog.Accepted: return tagname, value = self._add_tag_dialog.getTag() if tagname is None or value is None: return None dprint(1, "tagging selected sources with", tagname, value) # tag selected sources for src in self.model.sources: if src.selected: src.setAttribute(tagname, value) # If tag is not new, set a UpdateSelectionOnly flag on the signal dprint(1, "adding tag to model") self.model.addTag(tagname) dprint(1, "recomputing totals") self.model.getTagGrouping(tagname).computeTotal(self.model.sources) dprint(1, "emitting update signal") what = SkyModel.SkyModel.UpdateSourceContent + SkyModel.SkyModel.UpdateTags + SkyModel.SkyModel.UpdateSelectionOnly self.model.emitUpdate(what, origin=self) def removeTagsFromSelection(self): if not hasattr(self, '_remove_tag_dialog'): self._remove_tag_dialog = Widgets.SelectTagsDialog( self, modal=True, caption="Remove Tags", ok_button="Remove") # get set of all tags in selected sources tags = set() for src in self.model.sources: if src.selected: tags.update(src.getTagNames()) if not tags: return tags = list(tags) tags.sort() # show dialog self._remove_tag_dialog.setTags(tags) if self._remove_tag_dialog.exec_() != QDialog.Accepted: return tags = self._remove_tag_dialog.getSelectedTags() if not tags: return # ask for confirmation plural = (len(tags) > 1 and "s") or "" if QMessageBox.question( self, "Removing tags", "<P>Really remove the tag%s '%s' from selected sources?</P>" % (plural, "', '".join(tags)), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) != QMessageBox.Yes: return # remove the tags for src in self.model.sources: if src.selected: for tag in tags: src.removeAttribute(tag) # update model self.model.scanTags() self.model.initGroupings() # emit signal what = SkyModel.SkyModel.UpdateSourceContent + SkyModel.SkyModel.UpdateTags + SkyModel.SkyModel.UpdateSelectionOnly self.model.emitUpdate(what, origin=self) def _indicateModelUpdated(self, what=None, origin=None, updated=True): """Marks model as updated.""" self._model_updated = updated self.isUpdated.emit(updated) if self.model: self.setWindowTitle("Tigger - %s%s" % ((self._display_filename or "(unnamed)", " (modified)" if updated else "")))