%PDF- %PDF-
Direktori : /lib/calibre/calibre/gui2/ |
Current File : //lib/calibre/calibre/gui2/open_with.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' import os import uuid from contextlib import suppress from functools import partial from qt.core import ( QAction, QBuffer, QByteArray, QIcon, QInputDialog, QKeySequence, QLabel, QListWidget, QListWidgetItem, QPixmap, QSize, QStackedLayout, Qt, QVBoxLayout, QWidget, pyqtSignal, QIODevice, QDialogButtonBox ) from threading import Thread from calibre import as_unicode from calibre.constants import ismacos, iswindows from calibre.gui2 import ( Application, choose_files, choose_images, choose_osx_app, elided_text, error_dialog, sanitize_env_vars ) from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.widgets2 import Dialog from calibre.utils.config import JSONConfig from calibre.utils.icu import numeric_sort_key as sort_key from polyglot.builtins import iteritems, string_or_bytes ENTRY_ROLE = Qt.ItemDataRole.UserRole def pixmap_to_data(pixmap): ba = QByteArray() buf = QBuffer(ba) buf.open(QIODevice.OpenModeFlag.WriteOnly) pixmap.save(buf, 'PNG') return bytearray(ba.data()) def run_program(entry, path, parent): import subprocess cmdline = entry_to_cmdline(entry, path) print('Running Open With commandline:', repr(cmdline)) try: with sanitize_env_vars(): process = subprocess.Popen(cmdline) except Exception as err: return error_dialog( parent, _('Failed to run'), _( 'Failed to run program, click "Show details" for more information'), det_msg='Command line: %r\n%s' %(cmdline, as_unicode(err))) t = Thread(name='WaitProgram', target=process.wait) t.daemon = True t.start() def entry_to_icon_text(entry, only_text=False): if only_text: return entry.get('name', entry.get('Name')) or _('Unknown') data = entry.get('icon_data') if isinstance(data, str): with suppress(Exception): from base64 import standard_b64decode data = bytearray(standard_b64decode(data)) if not isinstance(data, (bytearray, bytes)): icon = QIcon(I('blank.png')) else: pmap = QPixmap() pmap.loadFromData(bytes(data)) if pmap.isNull(): icon = QIcon(I('blank.png')) else: icon = QIcon(pmap) return icon, entry.get('name', entry.get('Name')) or _('Unknown') if iswindows: # Windows {{{ import subprocess from calibre.utils.open_with.windows import ( load_icon_for_cmdline, load_icon_resource ) from calibre.utils.winreg.default_programs import ( find_programs, friendly_app_name ) from calibre_extensions import winutil oprefs = JSONConfig('windows_open_with') def entry_sort_key(entry): return sort_key(entry.get('name') or '') def icon_for_entry(entry, delete_icon_resource=False, as_data=False): res = entry.pop('icon_resource', None) if delete_icon_resource else entry.get('icon_resource') if res is None: return load_icon_for_cmdline(entry['cmdline'], as_data=as_data) try: return load_icon_resource(res, as_data=as_data) except Exception: import traceback traceback.print_exc() return load_icon_for_cmdline(entry['cmdline'], as_data=as_data) def finalize_entry(entry): try: data = icon_for_entry(entry, delete_icon_resource=True, as_data=True) except Exception: data = None import traceback traceback.print_exc() if isinstance(data, (bytes, bytearray)) or data is None: entry['icon_data'] = data return entry def change_name_in_entry(entry, newname): entry['name'] = newname def entry_to_item(entry, parent): try: icon = icon_for_entry(entry) except Exception: icon = None import traceback traceback.print_exc() if not icon: icon = entry_to_icon_text(entry)[0] ans = QListWidgetItem(QIcon(icon), entry.get('name') or _('Unknown'), parent) ans.setData(ENTRY_ROLE, entry) ans.setToolTip(_('Command line:') + '\n' + entry['cmdline']) def choose_manually(filetype, parent): ans = choose_files( parent, 'choose-open-with-program-manually-win', _('Choose a program to open %s files') % filetype.upper(), filters=[(_('Executable files'), ['exe', 'bat', 'com', 'cmd'])], select_only_single_file=True) if ans: ans = os.path.abspath(ans[0]) if not os.access(ans, os.X_OK): error_dialog(parent, _('Cannot execute'), _( 'The program %s is not an executable file') % ans, show=True) return qans = ans.replace('"', r'\"') name = friendly_app_name(exe=ans) or os.path.splitext(os.path.basename(ans))[0] return {'cmdline':'"%s" "%%1"' % qans, 'name':name} def entry_to_cmdline(entry, path): cmdline = entry['cmdline'] qpath = path.replace('"', r'\"') return cmdline.replace('%1', qpath) del run_program def run_program(entry, path, parent): # noqa import re cmdline = entry_to_cmdline(entry, path) flags = subprocess.CREATE_DEFAULT_ERROR_MODE | subprocess.CREATE_NEW_PROCESS_GROUP if re.match(r'"[^"]+?(.bat|.cmd|.com)"', cmdline, flags=re.I): flags |= subprocess.CREATE_NO_WINDOW console = ' (console)' else: flags |= subprocess.DETACHED_PROCESS console = '' print('Running Open With commandline%s:' % console, repr(entry['cmdline']), ' |==> ', repr(cmdline)) try: with sanitize_env_vars(): winutil.run_cmdline(cmdline, flags, 2000) except Exception as err: return error_dialog( parent, _('Failed to run'), _( 'Failed to run program, click "Show details" for more information'), det_msg='Command line: %r\n%s' %(cmdline, as_unicode(err))) # }}} elif ismacos: # macOS {{{ oprefs = JSONConfig('osx_open_with') from calibre.utils.open_with.osx import ( entry_to_cmdline, find_programs, get_bundle_data, get_icon ) def entry_sort_key(entry): return sort_key(entry.get('name') or '') def finalize_entry(entry): entry['extensions'] = tuple(entry.get('extensions', ())) data = get_icon(entry.pop('icon_file', None), as_data=True, pixmap_to_data=pixmap_to_data) if data: entry['icon_data'] = data return entry def change_name_in_entry(entry, newname): entry['name'] = newname def entry_to_item(entry, parent): icon = get_icon(entry.get('icon_file'), as_data=False) if icon is None: icon = entry_to_icon_text(entry)[0] else: icon = QPixmap.fromImage(icon) ans = QListWidgetItem(QIcon(icon), entry.get('name') or _('Unknown'), parent) ans.setData(ENTRY_ROLE, entry) ans.setToolTip(_('Application path:') + '\n' + entry['path']) def choose_manually(filetype, parent): ans = choose_osx_app(parent, 'choose-open-with-program-manually', _('Choose a program to open %s files') % filetype.upper()) if ans: ans = ans[0] if os.path.isdir(ans): app = get_bundle_data(ans) if app is None: error_dialog(parent, _('Invalid application'), _( '%s is not a valid macOS application bundle.') % ans, show=True) return return app if not os.access(ans, os.X_OK): error_dialog(parent, _('Cannot execute'), _( 'The program %s is not an executable file') % ans, show=True) return return {'path':ans, 'name': os.path.basename(ans)} # }}} else: # XDG {{{ oprefs = JSONConfig('xdg_open_with') from calibre.utils.open_with.linux import ( entry_sort_key, entry_to_cmdline, find_programs ) def change_name_in_entry(entry, newname): entry['Name'] = newname def entry_to_item(entry, parent): icon_path = entry.get('Icon') or I('blank.png') if not isinstance(icon_path, string_or_bytes): icon_path = I('blank.png') ans = QListWidgetItem(QIcon(icon_path), entry.get('Name') or _('Unknown'), parent) ans.setData(ENTRY_ROLE, entry) comment = (entry.get('Comment') or '') if comment: comment += '\n' ans.setToolTip(comment + _('Command line:') + '\n' + (' '.join(entry['Exec']))) def choose_manually(filetype, parent): dd = '/usr/bin' if os.path.isdir('/usr/bin') else '~' ans = choose_files(parent, 'choose-open-with-program-manually', _('Choose a program to open %s files') % filetype.upper(), select_only_single_file=True, default_dir=dd) if ans: ans = ans[0] if not os.access(ans, os.X_OK): error_dialog(parent, _('Cannot execute'), _( 'The program %s is not an executable file') % ans, show=True) return return {'Exec':[ans, '%f'], 'Name':os.path.basename(ans)} def finalize_entry(entry): icon_path = entry.get('Icon') if icon_path: ic = QIcon(icon_path) if not ic.isNull(): pmap = ic.pixmap(48, 48) if not pmap.isNull(): entry['icon_data'] = pixmap_to_data(pmap) try: entry['MimeType'] = tuple(entry['MimeType']) except KeyError: entry['MimeType'] = () return entry # }}} class ChooseProgram(Dialog): # {{{ found = pyqtSignal() def __init__(self, file_type='jpeg', parent=None, prefs=oprefs): self.file_type = file_type self.programs = self.find_error = self.selected_entry = None self.select_manually = False Dialog.__init__(self, _('Choose a program'), 'choose-open-with-program-dialog', parent=parent, prefs=prefs) self.found.connect(self.programs_found, type=Qt.ConnectionType.QueuedConnection) self.pi.startAnimation() t = Thread(target=self.find_programs) t.daemon = True t.start() def setup_ui(self): self.stacks = s = QStackedLayout(self) self.w = w = QWidget(self) self.w.l = l = QVBoxLayout(w) self.pi = pi = ProgressIndicator(self, 256) l.addStretch(1), l.addWidget(pi, alignment=Qt.AlignmentFlag.AlignHCenter), l.addSpacing(10) w.la = la = QLabel(_('Gathering data, please wait...')) f = la.font() f.setBold(True), f.setPointSize(28), la.setFont(f) l.addWidget(la, alignment=Qt.AlignmentFlag.AlignHCenter), l.addStretch(1) s.addWidget(w) self.w2 = w = QWidget(self) self.l = l = QVBoxLayout(w) s.addWidget(w) self.la = la = QLabel(_('Choose a program to open %s files') % self.file_type.upper()) self.plist = pl = QListWidget(self) pl.doubleClicked.connect(self.accept) pl.setIconSize(QSize(48, 48)), pl.setSpacing(5) pl.doubleClicked.connect(self.accept) l.addWidget(la), l.addWidget(pl) la.setBuddy(pl) b = self.bb.addButton(_('&Browse computer for program'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.manual) l.addWidget(self.bb) def sizeHint(self): return QSize(600, 500) def find_programs(self): try: self.programs = find_programs(self.file_type.split()) except Exception: import traceback self.find_error = traceback.print_exc() self.found.emit() def programs_found(self): if self.find_error is not None: error_dialog(self, _('Error finding programs'), _( 'Failed to find programs on your computer, click "Show details" for' ' more information'), det_msg=self.find_error, show=True) self.select_manually = True return self.reject() if not self.programs: self.select_manually = True return self.reject() for entry in self.programs: entry_to_item(entry, self.plist) self.stacks.setCurrentIndex(1) def accept(self): ci = self.plist.currentItem() if ci is not None: self.selected_entry = ci.data(ENTRY_ROLE) return Dialog.accept(self) def manual(self): self.select_manually = True self.reject() oprefs.defaults['entries'] = {} def choose_program(file_type='jpeg', parent=None, prefs=oprefs): oft = file_type = file_type.lower() file_type = {'cover_image':'jpeg'}.get(oft, oft) d = ChooseProgram(file_type, parent, prefs) d.exec() entry = choose_manually(file_type, parent) if d.select_manually else d.selected_entry if entry is not None: entry = finalize_entry(entry) entry['uuid'] = str(uuid.uuid4()) entries = oprefs['entries'] if oft not in entries: entries[oft] = [] entries[oft].append(entry) entries[oft].sort(key=entry_sort_key) oprefs['entries'] = entries register_keyboard_shortcuts(finalize=True) return entry def populate_menu(menu, connect_action, file_type): file_type = file_type.lower() for entry in oprefs['entries'].get(file_type, ()): icon, text = entry_to_icon_text(entry) text = elided_text(text, pos='right') sa = registered_shortcuts.get(entry['uuid']) if sa is not None: text += '\t' + sa.shortcut().toString(QKeySequence.SequenceFormat.NativeText) ac = menu.addAction(icon, text) connect_action(ac, entry) return menu # }}} class EditPrograms(Dialog): # {{{ def __init__(self, file_type='jpeg', parent=None): self.file_type = file_type.lower() Dialog.__init__(self, _('Edit the applications used for %s files') % file_type.upper(), 'edit-open-with-programs', parent=parent) def setup_ui(self): self.l = l = QVBoxLayout(self) self.plist = pl = QListWidget(self) pl.setIconSize(QSize(48, 48)), pl.setSpacing(5) l.addWidget(pl) self.bb.clear(), self.bb.setStandardButtons(QDialogButtonBox.StandardButton.Close) self.rb = b = self.bb.addButton(_('&Remove'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.remove), b.setIcon(QIcon(I('list_remove.png'))) self.cb = b = self.bb.addButton(_('Change &icon'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.change_icon), b.setIcon(QIcon(I('icon_choose.png'))) self.cb = b = self.bb.addButton(_('Change &name'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.change_name), b.setIcon(QIcon(I('modified.png'))) l.addWidget(self.bb) self.populate() def sizeHint(self): return QSize(600, 400) def populate(self): self.plist.clear() for entry in oprefs['entries'].get(self.file_type, ()): entry_to_item(entry, self.plist) def change_icon(self): ci = self.plist.currentItem() if ci is None: return error_dialog(self, _('No selection'), _( 'No application selected'), show=True) paths = choose_images(self, 'choose-new-icon-for-open-with-program', _( 'Choose new icon')) if paths: ic = QIcon(paths[0]) if ic.isNull(): return error_dialog(self, _('Invalid icon'), _( 'Could not load image from %s') % paths[0], show=True) pmap = ic.pixmap(48, 48) if not pmap.isNull(): entry = ci.data(ENTRY_ROLE) entry['icon_data'] = pixmap_to_data(pmap) ci.setData(ENTRY_ROLE, entry) self.update_stored_config() ci.setIcon(ic) def change_name(self): ci = self.plist.currentItem() if ci is None: return error_dialog(self, _('No selection'), _( 'No application selected'), show=True) name = ci.data(Qt.ItemDataRole.DisplayRole) name, ok = QInputDialog.getText(self, _('Enter new name'), _('New name for {}').format(name), text=name) if ok and name: entry = ci.data(ENTRY_ROLE) change_name_in_entry(entry, name) ci.setData(ENTRY_ROLE, entry) self.update_stored_config() ci.setData(Qt.ItemDataRole.DisplayRole, name) def remove(self): ci = self.plist.currentItem() if ci is None: return error_dialog(self, _('No selection'), _( 'No application selected'), show=True) row = self.plist.row(ci) self.plist.takeItem(row) self.update_stored_config() register_keyboard_shortcuts(finalize=True) def update_stored_config(self): entries = [self.plist.item(i).data(ENTRY_ROLE) for i in range(self.plist.count())] oprefs['entries'][self.file_type] = entries oprefs['entries'] = oprefs['entries'] def edit_programs(file_type, parent): d = EditPrograms(file_type, parent) d.exec() # }}} registered_shortcuts = {} def register_keyboard_shortcuts(gui=None, finalize=False): if gui is None: from calibre.gui2.ui import get_gui gui = get_gui() if gui is None: return for unique_name, action in iteritems(registered_shortcuts): gui.keyboard.unregister_shortcut(unique_name) gui.removeAction(action) registered_shortcuts.clear() for filetype, applications in iteritems(oprefs['entries']): for application in applications: text = entry_to_icon_text(application, only_text=True) t = _('cover image') if filetype.upper() == 'COVER_IMAGE' else filetype.upper() name = _('Open {0} files with {1}').format(t, text) ac = QAction(gui) unique_name = application['uuid'] func = partial(gui.open_with_action_triggerred, filetype, application) ac.triggered.connect(func) gui.keyboard.register_shortcut(unique_name, name, action=ac, group=_('Open with')) gui.addAction(ac) registered_shortcuts[unique_name] = ac if finalize: gui.keyboard.finalize() if __name__ == '__main__': from pprint import pprint app = Application([]) pprint(choose_program('pdf')) del app