Wire up Open With

This commit is contained in:
Kovid Goyal 2015-02-26 16:21:07 +05:30
parent d96c632028
commit 1451573bc8
3 changed files with 191 additions and 26 deletions

View File

@ -6,6 +6,7 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from binascii import unhexlify from binascii import unhexlify
from functools import partial
from PyQt5.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, QIcon, from PyQt5.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, QIcon,
QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, QAction, QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, QAction,
@ -107,6 +108,7 @@ class CoverView(QWidget): # {{{
cover_changed = pyqtSignal(object, object) cover_changed = pyqtSignal(object, object)
cover_removed = pyqtSignal(object) cover_removed = pyqtSignal(object)
open_cover_with = pyqtSignal(object, object)
def __init__(self, vertical, parent=None): def __init__(self, vertical, parent=None):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
@ -203,6 +205,7 @@ class CoverView(QWidget): # {{{
) )
def contextMenuEvent(self, ev): def contextMenuEvent(self, ev):
from calibre.gui2.open_with import populate_menu
cm = QMenu(self) cm = QMenu(self)
paste = cm.addAction(_('Paste Cover')) paste = cm.addAction(_('Paste Cover'))
copy = cm.addAction(_('Copy Cover')) copy = cm.addAction(_('Copy Cover'))
@ -214,8 +217,28 @@ class CoverView(QWidget): # {{{
paste.triggered.connect(self.paste_from_clipboard) paste.triggered.connect(self.paste_from_clipboard)
remove.triggered.connect(self.remove_cover) remove.triggered.connect(self.remove_cover)
gc.triggered.connect(self.generate_cover) gc.triggered.connect(self.generate_cover)
m = QMenu(_('Open with...'))
populate_menu(m, self.open_with, 'jpeg')
if len(m.actions()) == 0:
cm.addAction(_('Open with...'), self.choose_open_with)
else:
m.addSeparator()
m.addAction(_('Choose other program...'), self.choose_open_with)
cm.addMenu(m)
cm.exec_(ev.globalPos()) cm.exec_(ev.globalPos())
def open_with(self, entry):
id_ = self.data.get('id', None)
if id_ is not None:
self.open_cover_with.emit(id_, entry)
def choose_open_with(self):
from calibre.gui2.open_with import choose_program
entry = choose_program('jpeg', self)
if entry is not None:
self.open_with(entry)
def copy_to_clipboard(self): def copy_to_clipboard(self):
QApplication.instance().clipboard().setPixmap(self.pixmap) QApplication.instance().clipboard().setPixmap(self.pixmap)
@ -286,6 +309,7 @@ class BookInfo(QWebView):
compare_format = pyqtSignal(int, object) compare_format = pyqtSignal(int, object)
copy_link = pyqtSignal(object) copy_link = pyqtSignal(object)
manage_author = pyqtSignal(object) manage_author = pyqtSignal(object)
open_fmt_with = pyqtSignal(int, object, object)
def __init__(self, vertical, parent=None): def __init__(self, vertical, parent=None):
QWebView.__init__(self, parent) QWebView.__init__(self, parent)
@ -405,7 +429,16 @@ class BookInfo(QWebView):
ac.current_fmt = (book_id, fmt) ac.current_fmt = (book_id, fmt)
ac.setText(t) ac.setText(t)
menu.addAction(ac) menu.addAction(ac)
if not fmt.upper().startswith('ORIGINAL_'):
from calibre.gui2.open_with import populate_menu
m = QMenu(_('Open with...'))
populate_menu(m, partial(self.open_with, book_id, fmt), fmt)
if len(m.actions()) == 0:
menu.addAction(_('Open with...'), partial(self.choose_open_with, book_id, fmt))
else:
m.addSeparator()
m.addAction(_('Choose other program...'), partial(self.choose_open_with, book_id, fmt))
menu.addMenu(m)
else: else:
el = r.linkElement() el = r.linkElement()
author = el.toPlainText() if unicode(el.attribute('calibre-data')) == u'authors' else None author = el.toPlainText() if unicode(el.attribute('calibre-data')) == u'authors' else None
@ -425,6 +458,15 @@ class BookInfo(QWebView):
if len(menu.actions()) > 0: if len(menu.actions()) > 0:
menu.exec_(ev.globalPos()) menu.exec_(ev.globalPos())
def open_with(self, book_id, fmt, entry):
self.open_fmt_with.emit(book_id, fmt, entry)
def choose_open_with(self, book_id, fmt):
from calibre.gui2.open_with import choose_program
entry = choose_program(fmt, self)
if entry is not None:
self.open_with(book_id, fmt, entry)
# }}} # }}}
@ -531,9 +573,11 @@ class BookDetails(QWidget): # {{{
remote_file_dropped = pyqtSignal(object, object) remote_file_dropped = pyqtSignal(object, object)
files_dropped = pyqtSignal(object, object) files_dropped = pyqtSignal(object, object)
cover_changed = pyqtSignal(object, object) cover_changed = pyqtSignal(object, object)
open_cover_with = pyqtSignal(object, object)
cover_removed = pyqtSignal(object) cover_removed = pyqtSignal(object)
view_device_book = pyqtSignal(object) view_device_book = pyqtSignal(object)
manage_author = pyqtSignal(object) manage_author = pyqtSignal(object)
open_fmt_with = pyqtSignal(int, object, object)
# Drag 'n drop {{{ # Drag 'n drop {{{
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
@ -590,12 +634,14 @@ class BookDetails(QWidget): # {{{
self.cover_view = CoverView(vertical, self) self.cover_view = CoverView(vertical, self)
self.cover_view.cover_changed.connect(self.cover_changed.emit) self.cover_view.cover_changed.connect(self.cover_changed.emit)
self.cover_view.open_cover_with.connect(self.open_cover_with.emit)
self.cover_view.cover_removed.connect(self.cover_removed.emit) self.cover_view.cover_removed.connect(self.cover_removed.emit)
self._layout.addWidget(self.cover_view) self._layout.addWidget(self.cover_view)
self.book_info = BookInfo(vertical, self) self.book_info = BookInfo(vertical, self)
self._layout.addWidget(self.book_info) self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self.handle_click) self.book_info.link_clicked.connect(self.handle_click)
self.book_info.remove_format.connect(self.remove_specific_format) self.book_info.remove_format.connect(self.remove_specific_format)
self.book_info.open_fmt_with.connect(self.open_fmt_with)
self.book_info.save_format.connect(self.save_specific_format) self.book_info.save_format.connect(self.save_specific_format)
self.book_info.restore_format.connect(self.restore_specific_format) self.book_info.restore_format.connect(self.restore_specific_format)
self.book_info.compare_format.connect(self.compare_specific_format) self.book_info.compare_format.connect(self.compare_specific_format)
@ -640,4 +686,3 @@ class BookDetails(QWidget): # {{{
self.show_data(Metadata(_('Unknown'))) self.show_data(Metadata(_('Unknown')))
# }}} # }}}

View File

@ -100,7 +100,7 @@ class LibraryViewMixin(object): # {{{
# }}} # }}}
class QuickviewSplitter(QSplitter): # {{{ class QuickviewSplitter(QSplitter): # {{{
def __init__(self, parent=None, orientation=Qt.Vertical, qv_widget=None): def __init__(self, parent=None, orientation=Qt.Vertical, qv_widget=None):
QSplitter.__init__(self, parent=parent, orientation=orientation) QSplitter.__init__(self, parent=parent, orientation=orientation)
@ -489,6 +489,10 @@ class LayoutMixin(object): # {{{
self.book_details.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book) self.book_details.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book)
self.book_details.cover_changed.connect(self.bd_cover_changed, self.book_details.cover_changed.connect(self.bd_cover_changed,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.book_details.open_cover_with.connect(self.bd_open_cover_with,
type=Qt.QueuedConnection)
self.book_details.open_fmt_with.connect(self.bd_open_fmt_with,
type=Qt.QueuedConnection)
self.book_details.cover_removed.connect(self.bd_cover_removed, self.book_details.cover_removed.connect(self.bd_cover_removed,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.book_details.remote_file_dropped.connect( self.book_details.remote_file_dropped.connect(
@ -525,6 +529,18 @@ class LayoutMixin(object): # {{{
if self.cover_flow: if self.cover_flow:
self.cover_flow.dataChanged() self.cover_flow.dataChanged()
def bd_open_cover_with(self, book_id, entry):
cpath = self.current_db.new_api.format_abspath(book_id, '__COVER_INTERNAL__')
if cpath:
from calibre.gui2.open_with import run_program
run_program(entry, cpath, self)
def bd_open_fmt_with(self, book_id, fmt, entry):
path = self.current_db.new_api.format_abspath(book_id, fmt)
if path:
from calibre.gui2.open_with import run_program
run_program(entry, path, self)
def bd_cover_removed(self, id_): def bd_cover_removed(self, id_):
self.library_view.model().db.remove_cover(id_, commit=True, self.library_view.model().db.remove_cover(id_, commit=True,
notify=False) notify=False)
@ -570,5 +586,3 @@ class LayoutMixin(object): # {{{
self.status_bar.update_state(library_total, total, current, selected) self.status_bar.update_state(library_total, total, current, selected)
# }}} # }}}

View File

@ -8,18 +8,59 @@ __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re, shlex, cPickle import os, re, shlex, cPickle
from collections import defaultdict from collections import defaultdict
from threading import Thread
from functools import partial
from calibre import force_unicode, walk, guess_type, prints from PyQt5.Qt import (
QApplication, QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt,
QListWidget, QSize, pyqtSignal, QListWidgetItem, QIcon, QByteArray,
QBuffer, QPixmap)
from calibre import force_unicode, walk, guess_type, prints, as_unicode
from calibre.constants import iswindows, isosx, filesystem_encoding, cache_dir from calibre.constants import iswindows, isosx, filesystem_encoding, cache_dir
from calibre.gui2 import error_dialog, choose_files
from calibre.gui2.widgets2 import Dialog
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.utils.config import JSONConfig
from calibre.utils.icu import numeric_sort_key as sort_key from calibre.utils.icu import numeric_sort_key as sort_key
from calibre.utils.localization import canonicalize_lang, get_lang from calibre.utils.localization import canonicalize_lang, get_lang
DESC_ROLE = Qt.UserRole
ENTRY_ROLE = DESC_ROLE + 1
def pixmap_to_data(pixmap):
ba = QByteArray()
buf = QBuffer(ba)
buf.open(QBuffer.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:
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()
if iswindows: if iswindows:
pass oprefs = JSONConfig('windows_open_with')
run_program
def run_program(entry, path, parent):
raise NotImplementedError()
elif isosx: elif isosx:
pass oprefs = JSONConfig('osx_open_with')
else: else:
# Linux find_programs {{{ oprefs = JSONConfig('xdg_open_with')
# XDG find_programs {{{
def parse_localized_key(key): def parse_localized_key(key):
name, rest = key.partition('[')[0::2] name, rest = key.partition('[')[0::2]
if not rest: if not rest:
@ -42,6 +83,7 @@ else:
return return
group = None group = None
ans = {} ans = {}
ans['desktop_file_path'] = path
for line in raw.splitlines(): for line in raw.splitlines():
m = gpat.match(line) m = gpat.match(line)
if m is not None: if m is not None:
@ -207,25 +249,61 @@ else:
if comment: if comment:
comment += '\n' comment += '\n'
ans.setToolTip(comment + _('Command line:') + '\n' + (' '.join(entry['Exec']))) ans.setToolTip(comment + _('Command line:') + '\n' + (' '.join(entry['Exec'])))
def choose_manually(filetype, parent):
ans = choose_files(parent, 'choose-open-with-program-manually', _('Choose a program to open %s files') % filetype.upper(), select_only_single_file=True)
if ans:
ans = ans[0]
if not os.access(ans, os.X_OK):
return error_dialog(parent, _('Cannot execute'), _(
'The program %s is not an executable file') % ans, show=True)
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)
entry['MimeType'] = tuple(entry['MimeType'])
return entry
def entry_sort_key(entry):
return sort_key(entry['Name'])
def entry_to_icon_text(entry):
data = entry.get('icon_data')
if data is None:
icon = QIcon(I('blank.png'))
else:
pmap = QPixmap()
pmap.loadFromData(bytes(data))
icon = QIcon(pmap)
return icon, entry['Name']
def entry_to_cmdline(entry, path):
path = os.path.abspath(path)
rmap = {
'f':path, 'F':path, 'u':'file://'+path, 'U':'file://'+path, '%':'%',
'i':entry.get('Icon', '%i'), 'c':entry.get('Name', ''), 'k':entry.get('desktop_file_path', ''),
}
def replace(match):
char = match.group()[-1]
repl = rmap.get(char)
return match.group() if repl is None else repl
sub = re.compile(r'%[fFuUdDnNickvm%]').sub
cmd = entry['Exec']
return cmd[:1] + [sub(replace, x) for x in cmd[1:]]
# }}} # }}}
from threading import Thread class ChooseProgram(Dialog): # {{{
from PyQt5.Qt import (
QApplication, QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt,
QListWidget, QSize, pyqtSignal, QListWidgetItem, QIcon)
from calibre.gui2 import gprefs, error_dialog
from calibre.gui2.widgets2 import Dialog
from calibre.gui2.progress_indicator import ProgressIndicator
DESC_ROLE = Qt.UserRole
ENTRY_ROLE = DESC_ROLE + 1
class ChooseProgram(Dialog):
found = pyqtSignal() found = pyqtSignal()
def __init__(self, file_type='jpeg', parent=None, prefs=gprefs): def __init__(self, file_type='jpeg', parent=None, prefs=oprefs):
self.file_type = file_type self.file_type = file_type
self.programs = self.find_error = self.selected_entry = None self.programs = self.find_error = self.selected_entry = None
self.select_manually = False self.select_manually = False
@ -258,6 +336,8 @@ class ChooseProgram(Dialog):
l.addWidget(la), l.addWidget(pl) l.addWidget(la), l.addWidget(pl)
la.setBuddy(pl) la.setBuddy(pl)
b = self.bb.addButton(_('&Browse computer for program'), self.bb.ActionRole)
b.clicked.connect(self.manual)
l.addWidget(self.bb) l.addWidget(self.bb)
def sizeHint(self): def sizeHint(self):
@ -291,10 +371,36 @@ class ChooseProgram(Dialog):
self.selected_entry = ci.data(ENTRY_ROLE) self.selected_entry = ci.data(ENTRY_ROLE)
return Dialog.accept(self) 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):
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)
entries = oprefs['entries']
if file_type not in entries:
entries[file_type] = []
entries[file_type].append(entry)
entries[file_type].sort(key=entry_sort_key)
oprefs['entries'] = entries
return entry
def populate_menu(menu, receiver, file_type):
for entry in oprefs['entries'].get(file_type, ()):
ac = menu.addAction(*entry_to_icon_text(entry))
ac.triggered.connect(partial(receiver, entry))
return menu
# }}}
if __name__ == '__main__': if __name__ == '__main__':
from pprint import pprint from pprint import pprint
app = QApplication([]) app = QApplication([])
d = ChooseProgram() pprint(choose_program('pdf'))
d.exec_()
pprint(d.selected_entry)
del app del app