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 PyQt5.Qt import (
|
||||
QApplication, QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt,
|
||||
QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt,
|
||||
QListWidget, QSize, pyqtSignal, QListWidgetItem, QIcon, QByteArray,
|
||||
QBuffer, QPixmap, QAction, QKeySequence)
|
||||
|
||||
from calibre import as_unicode
|
||||
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.progress_indicator import ProgressIndicator
|
||||
from calibre.utils.config import JSONConfig
|
||||
@ -123,7 +123,34 @@ if iswindows:
|
||||
# }}}
|
||||
|
||||
elif isosx:
|
||||
# OS X {{{
|
||||
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:
|
||||
# XDG {{{
|
||||
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.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)
|
||||
@ -368,6 +396,6 @@ def register_keyboard_shortcuts(gui=None, finalize=False):
|
||||
|
||||
if __name__ == '__main__':
|
||||
from pprint import pprint
|
||||
app = QApplication([])
|
||||
app = Application([])
|
||||
pprint(choose_program('pdf'))
|
||||
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