mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Start work on OS X implementation of Open With
This commit is contained in:
parent
9e4b1f71b9
commit
26e4ca1852
@ -11,13 +11,13 @@ from threading import Thread
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt5.Qt import (
|
from PyQt5.Qt import (
|
||||||
QApplication, QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt,
|
QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt,
|
||||||
QListWidget, QSize, pyqtSignal, QListWidgetItem, QIcon, QByteArray,
|
QListWidget, QSize, pyqtSignal, QListWidgetItem, QIcon, QByteArray,
|
||||||
QBuffer, QPixmap, QAction, QKeySequence)
|
QBuffer, QPixmap, QAction, QKeySequence)
|
||||||
|
|
||||||
from calibre import as_unicode
|
from calibre import as_unicode
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx
|
||||||
from calibre.gui2 import error_dialog, choose_files, choose_images, elided_text, sanitize_env_vars
|
from calibre.gui2 import error_dialog, choose_files, choose_images, elided_text, sanitize_env_vars, Application
|
||||||
from calibre.gui2.widgets2 import Dialog
|
from calibre.gui2.widgets2 import Dialog
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
from calibre.utils.config import JSONConfig
|
from calibre.utils.config import JSONConfig
|
||||||
@ -123,7 +123,34 @@ if iswindows:
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
elif isosx:
|
elif isosx:
|
||||||
|
# OS X {{{
|
||||||
oprefs = JSONConfig('osx_open_with')
|
oprefs = JSONConfig('osx_open_with')
|
||||||
|
from calibre.utils.open_with.osx import find_programs, get_icon, entry_to_cmdline
|
||||||
|
|
||||||
|
def entry_sort_key(entry):
|
||||||
|
return sort_key(entry.get('name') or '')
|
||||||
|
|
||||||
|
def finalize_entry(entry):
|
||||||
|
entry['extensions'] = tuple(entry['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 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):
|
||||||
|
raise NotImplementedError()
|
||||||
|
# }}}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# XDG {{{
|
# XDG {{{
|
||||||
oprefs = JSONConfig('xdg_open_with')
|
oprefs = JSONConfig('xdg_open_with')
|
||||||
@ -191,6 +218,7 @@ class ChooseProgram(Dialog): # {{{
|
|||||||
|
|
||||||
self.la = la = QLabel(_('Choose a program to open %s files') % self.file_type.upper())
|
self.la = la = QLabel(_('Choose a program to open %s files') % self.file_type.upper())
|
||||||
self.plist = pl = QListWidget(self)
|
self.plist = pl = QListWidget(self)
|
||||||
|
pl.doubleClicked.connect(self.accept)
|
||||||
pl.setIconSize(QSize(48, 48)), pl.setSpacing(5)
|
pl.setIconSize(QSize(48, 48)), pl.setSpacing(5)
|
||||||
pl.doubleClicked.connect(self.accept)
|
pl.doubleClicked.connect(self.accept)
|
||||||
l.addWidget(la), l.addWidget(pl)
|
l.addWidget(la), l.addWidget(pl)
|
||||||
@ -368,6 +396,6 @@ def register_keyboard_shortcuts(gui=None, finalize=False):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
app = QApplication([])
|
app = Application([])
|
||||||
pprint(choose_program('pdf'))
|
pprint(choose_program('pdf'))
|
||||||
del app
|
del app
|
||||||
|
298
src/calibre/utils/open_with/osx.py
Normal file
298
src/calibre/utils/open_with/osx.py
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import os, plistlib, re, mimetypes, subprocess
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from calibre.ptempfile import TemporaryDirectory
|
||||||
|
from calibre.utils.icu import numeric_sort_key
|
||||||
|
|
||||||
|
application_locations = ('/Applications', '~/Applications', '~/Desktop')
|
||||||
|
|
||||||
|
# Public UTI MAP {{{
|
||||||
|
def generate_public_uti_map():
|
||||||
|
from lxml import etree
|
||||||
|
import html5lib, urllib
|
||||||
|
raw = urllib.urlopen(
|
||||||
|
'https://developer.apple.com/library/ios/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html').read()
|
||||||
|
root = html5lib.parse(raw, treebuilder='lxml', namespaceHTMLElements=False)
|
||||||
|
table = root.xpath('//table')[0]
|
||||||
|
data = {}
|
||||||
|
for tr in table.xpath('descendant::tr')[1:]:
|
||||||
|
td = tr.xpath('descendant::td')
|
||||||
|
identifier = etree.tostring(td[0], method='text', encoding=unicode).strip()
|
||||||
|
tags = etree.tostring(td[2], method='text', encoding=unicode).strip()
|
||||||
|
identifier = identifier.split()[0].replace('\u200b', '')
|
||||||
|
exts = [x.strip()[1:].lower() for x in tags.split(',') if x.strip().startswith('.')]
|
||||||
|
for ext in exts:
|
||||||
|
data[ext] = identifier
|
||||||
|
lines = ['PUBLIC_UTI_MAP = {']
|
||||||
|
for ext in sorted(data):
|
||||||
|
r = ("'" + ext + "':").ljust(16)
|
||||||
|
lines.append((' ' * 4) + r + "'" + data[ext] + "',")
|
||||||
|
lines.append('}')
|
||||||
|
with open(__file__, 'r+b') as f:
|
||||||
|
raw = f.read()
|
||||||
|
f.seek(0)
|
||||||
|
nraw = re.sub(r'^PUBLIC_UTI_MAP = .+?}', '\n'.join(lines), raw, flags=re.MULTILINE | re.DOTALL)
|
||||||
|
f.truncate(), f.write(nraw)
|
||||||
|
# Generated by generate_public_uti_map()
|
||||||
|
PUBLIC_UTI_MAP = {
|
||||||
|
'3g2': 'public.3gpp2',
|
||||||
|
'3gp': 'public.3gpp',
|
||||||
|
'3gp2': 'public.3gpp2',
|
||||||
|
'3gpp': 'public.3gpp',
|
||||||
|
'aif': 'public.aiff-audio',
|
||||||
|
'aifc': 'public.aifc-audio',
|
||||||
|
'aiff': 'public.aiff-audio',
|
||||||
|
'app': 'com.apple.application-bundle',
|
||||||
|
'applescript': 'com.apple.applescript.text',
|
||||||
|
'au': 'public.ulaw-audio',
|
||||||
|
'avi': 'public.avi',
|
||||||
|
'bin': 'com.apple.macbinary-archive',
|
||||||
|
'bundle': 'com.apple.bundle',
|
||||||
|
'c': 'public.c-source',
|
||||||
|
'c++': 'public.c-plus-plus-source',
|
||||||
|
'caf': 'com.apple.coreaudio-format',
|
||||||
|
'cc': 'public.c-plus-plus-source',
|
||||||
|
'class': 'com.sun.java-class',
|
||||||
|
'command': 'public.shell-script',
|
||||||
|
'cp': 'public.c-plus-plus-source',
|
||||||
|
'cpio': 'public.cpio-archive',
|
||||||
|
'cpp': 'public.c-plus-plus-source',
|
||||||
|
'csh': 'public.csh-script',
|
||||||
|
'cxx': 'public.c-plus-plus-source',
|
||||||
|
'defs': 'public.mig-source',
|
||||||
|
'dfont': 'com.apple.truetype-datafork-suitcase-font',
|
||||||
|
'dll': 'com.microsoft.windows-dynamic-link-library',
|
||||||
|
'exe': 'com.microsoft.windows-executable',
|
||||||
|
'exp': 'com.apple.symbol-export',
|
||||||
|
'framework': 'com.apple.framework',
|
||||||
|
'gtar': 'org.gnu.gnu-tar-archive',
|
||||||
|
'gz': 'org.gnu.gnu-zip-archive',
|
||||||
|
'gzip': 'org.gnu.gnu-zip-archive',
|
||||||
|
'h': 'public.c-header',
|
||||||
|
'h++': 'public.c-plus-plus-header',
|
||||||
|
'hpp': 'public.c-plus-plus-header',
|
||||||
|
'hqx': 'com.apple.binhex-archive',
|
||||||
|
'htm': 'public.html',
|
||||||
|
'html': 'public.html',
|
||||||
|
'hxx': 'public.c-plus-plus-header',
|
||||||
|
'icc': 'com.apple.colorsync-profile',
|
||||||
|
'icm': 'com.apple.colorsync-profile',
|
||||||
|
'icns': 'com.apple.icns',
|
||||||
|
'jar': 'com.sun.java-archive',
|
||||||
|
'jav': 'com.sun.java-source',
|
||||||
|
'java': 'com.sun.java-source',
|
||||||
|
'javascript': 'com.netscape.javascript-source',
|
||||||
|
'jnlp': 'com.sun.java-web-start',
|
||||||
|
'jp2': 'public.jpeg-2000',
|
||||||
|
'jpeg': 'public.jpeg',
|
||||||
|
'jpg': 'public.jpeg',
|
||||||
|
'js': 'com.netscape.javascript-source',
|
||||||
|
'jscript': 'com.netscape.javascript-source',
|
||||||
|
'm': 'public.objective-c-source',
|
||||||
|
'm15': 'public.mpeg',
|
||||||
|
'm4a': 'public.mpeg-4-audio',
|
||||||
|
'm4b': 'com.apple.protected-mpeg-4-audio',
|
||||||
|
'm4p': 'com.apple.protected-mpeg-4-audio',
|
||||||
|
'm75': 'public.mpeg',
|
||||||
|
'mdimporter': 'com.apple.metadata-importer',
|
||||||
|
'mig': 'public.mig-source',
|
||||||
|
'mm': 'public.objective-c-plus-plus-source',
|
||||||
|
'mov': 'com.apple.quicktime-movie',
|
||||||
|
'mp3': 'public.mp3',
|
||||||
|
'mp4': 'public.mpeg-4',
|
||||||
|
'mpeg': 'public.mpeg',
|
||||||
|
'mpg': 'public.mpeg',
|
||||||
|
'o': 'public.object-code',
|
||||||
|
'otf': 'public.opentype-font',
|
||||||
|
'pct': 'com.apple.pict',
|
||||||
|
'pf': 'com.apple.colorsync-profile',
|
||||||
|
'pfa': 'com.adobe.postscript.pfa-font',
|
||||||
|
'pfb': 'com.adobe.postscript-pfb-font',
|
||||||
|
'ph3': 'public.php-script',
|
||||||
|
'ph4': 'public.php-script',
|
||||||
|
'php': 'public.php-script',
|
||||||
|
'php3': 'public.php-script',
|
||||||
|
'php4': 'public.php-script',
|
||||||
|
'phtml': 'public.php-script',
|
||||||
|
'pic': 'com.apple.pict',
|
||||||
|
'pict': 'com.apple.pict',
|
||||||
|
'pl': 'public.perl-script',
|
||||||
|
'plugin': 'com.apple.plugin',
|
||||||
|
'pm': 'public.perl-script',
|
||||||
|
'png': 'public.png',
|
||||||
|
'pntg': 'com.apple.macpaint-image',
|
||||||
|
'py': 'public.python-script',
|
||||||
|
'qif': 'com.apple.quicktime-image',
|
||||||
|
'qt': 'com.apple.quicktime-movie',
|
||||||
|
'qtif': 'com.apple.quicktime-image',
|
||||||
|
'qtz': 'com.apple.quartz-composer-composition',
|
||||||
|
'r': 'com.apple.rez-source',
|
||||||
|
'rb': 'public.ruby-script',
|
||||||
|
'rbw': 'public.ruby-script',
|
||||||
|
'rtf': 'public.rtf',
|
||||||
|
'rtfd': 'com.apple.rtfd',
|
||||||
|
's': 'public.assembly-source',
|
||||||
|
'scpt': 'com.apple.applescript.script',
|
||||||
|
'sh': 'public.shell-script',
|
||||||
|
'snd': 'public.ulaw-audio',
|
||||||
|
'suit': 'com.apple.font-suitcase',
|
||||||
|
'tar': 'public.tar-archive',
|
||||||
|
'tgz': 'org.gnu.gnu-zip-tar-archive',
|
||||||
|
'tif': 'public.tiff',
|
||||||
|
'tiff': 'public.tiff',
|
||||||
|
'ttc': 'public.truetype-collection-font',
|
||||||
|
'ttf': 'public.truetype-ttf-font',
|
||||||
|
'txt': 'public.plain-text',
|
||||||
|
'ulw': 'public.ulaw-audio',
|
||||||
|
'vcard': 'public.vcard',
|
||||||
|
'vcf': 'public.vcard',
|
||||||
|
'vfw': 'public.avi',
|
||||||
|
'wdgt': 'com.apple.dashboard-widget',
|
||||||
|
'xbm': 'public.xbitmap-image',
|
||||||
|
'xml': 'public.xml',
|
||||||
|
'zip': 'com.pkware.zip-archive',
|
||||||
|
}
|
||||||
|
PUBLIC_UTI_RMAP = defaultdict(set)
|
||||||
|
for ext, uti in PUBLIC_UTI_MAP.iteritems():
|
||||||
|
PUBLIC_UTI_RMAP[uti].add(ext)
|
||||||
|
PUBLIC_UTI_RMAP = dict(PUBLIC_UTI_RMAP)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def find_applications_in(base):
|
||||||
|
try:
|
||||||
|
entries = os.listdir(base)
|
||||||
|
except EnvironmentError:
|
||||||
|
return
|
||||||
|
for name in entries:
|
||||||
|
path = os.path.join(base, name)
|
||||||
|
if os.path.isdir(path):
|
||||||
|
if name.lower().endswith('.app'):
|
||||||
|
yield path
|
||||||
|
else:
|
||||||
|
for app in find_applications_in(path):
|
||||||
|
yield app
|
||||||
|
|
||||||
|
def find_applications():
|
||||||
|
for base in application_locations:
|
||||||
|
base = os.path.expanduser(base)
|
||||||
|
for app in find_applications_in(base):
|
||||||
|
yield app
|
||||||
|
|
||||||
|
def get_extensions_from_utis(utis, plist):
|
||||||
|
declared_utis = defaultdict(set)
|
||||||
|
for key in ('UTExportedTypeDeclarations', 'UTImportedTypeDeclarations'):
|
||||||
|
for decl in plist.get(key, ()):
|
||||||
|
if isinstance(decl, dict):
|
||||||
|
uti = decl.get('UTTypeIdentifier')
|
||||||
|
if isinstance(uti, basestring):
|
||||||
|
spec = decl.get('UTTypeTagSpecification')
|
||||||
|
if isinstance(spec, dict):
|
||||||
|
ext = spec.get('public.filename-extension')
|
||||||
|
if ext:
|
||||||
|
declared_utis[uti] |= set(ext)
|
||||||
|
types = spec.get('public.mime-type')
|
||||||
|
if types:
|
||||||
|
for mt in types:
|
||||||
|
for ext in mimetypes.guess_all_extensions(mt, strict=False):
|
||||||
|
declared_utis[uti].add(ext.lower()[1:])
|
||||||
|
ans = set()
|
||||||
|
for uti in utis:
|
||||||
|
ans |= declared_utis[uti]
|
||||||
|
ans |= PUBLIC_UTI_RMAP.get(uti, set())
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def get_bundle_data(path):
|
||||||
|
path = os.path.abspath(path)
|
||||||
|
info = os.path.join(path, 'Contents', 'Info.plist')
|
||||||
|
ans = {
|
||||||
|
'name': os.path.splitext(os.path.basename(path))[0],
|
||||||
|
'path': path,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
plist = plistlib.readPlist(info)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
ans['name'] = plist.get('CFBundleDisplayName') or plist.get('CFBundleName') or ans['name']
|
||||||
|
icfile = plist.get('CFBundleIconFile')
|
||||||
|
if icfile:
|
||||||
|
icfile = os.path.join(path, 'Contents', 'Resources', icfile)
|
||||||
|
if not os.path.exists(icfile):
|
||||||
|
icfile += '.icns'
|
||||||
|
if os.path.exists(icfile):
|
||||||
|
ans['icon_file'] = icfile
|
||||||
|
bid = plist.get('CFBundleIdentifier')
|
||||||
|
if bid:
|
||||||
|
ans['identifier'] = bid
|
||||||
|
ans['extensions'] = extensions = set()
|
||||||
|
for dtype in plist.get('CFBundleDocumentTypes', ()):
|
||||||
|
utis = frozenset(dtype.get('LSItemContentTypes', ()))
|
||||||
|
if utis:
|
||||||
|
extensions |= get_extensions_from_utis(utis, plist)
|
||||||
|
else:
|
||||||
|
for ext in dtype.get('CFBundleTypeExtensions', ()):
|
||||||
|
if isinstance(ext, basestring):
|
||||||
|
extensions.add(ext.lower())
|
||||||
|
for mt in dtype.get('CFBundleTypeMIMETypes', ()):
|
||||||
|
if isinstance(mt, basestring):
|
||||||
|
for ext in mimetypes.guess_all_extensions(mt, strict=False):
|
||||||
|
extensions.add(ext.lower())
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def find_programs(extensions):
|
||||||
|
extensions = frozenset(extensions)
|
||||||
|
ans = []
|
||||||
|
for app in find_applications():
|
||||||
|
try:
|
||||||
|
app = get_bundle_data(app)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
continue
|
||||||
|
if app and app['extensions'].intersection(extensions):
|
||||||
|
ans.append(app)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def get_icon(path, pixmap_to_data=None, as_data=False, size=64):
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
with TemporaryDirectory() as tdir:
|
||||||
|
iconset = os.path.join(tdir, 'output.iconset')
|
||||||
|
try:
|
||||||
|
subprocess.check_call(['iconutil', '-c', 'iconset', '-o', 'output.iconset', path], cwd=tdir)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
names = os.listdir(iconset)
|
||||||
|
except EnvironmentError:
|
||||||
|
return
|
||||||
|
if not names:
|
||||||
|
return
|
||||||
|
from PyQt5.Qt import QImage, Qt
|
||||||
|
names.sort(key=numeric_sort_key)
|
||||||
|
for name in names:
|
||||||
|
m = re.search('(\d+)x\d+', name)
|
||||||
|
if m is not None and int(m.group(1)) >= size:
|
||||||
|
ans = QImage(os.path.join(iconset, name))
|
||||||
|
if not ans.isNull():
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
ans = ans.scaled(size, size, transformMode=Qt.SmoothTransformation)
|
||||||
|
if as_data:
|
||||||
|
ans = pixmap_to_data(ans)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def entry_to_cmdline(entry, path):
|
||||||
|
app = entry['path']
|
||||||
|
if not os.path.isdir(app) and 'identifier' in entry:
|
||||||
|
return ['open', '-b', entry['identifier'], path]
|
||||||
|
return ['open', '-a', app, path]
|
Loading…
x
Reference in New Issue
Block a user