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'
from binascii import unhexlify
from functools import partial
from PyQt5.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, QIcon,
QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, QAction,
@ -107,6 +108,7 @@ class CoverView(QWidget): # {{{
cover_changed = pyqtSignal(object, object)
cover_removed = pyqtSignal(object)
open_cover_with = pyqtSignal(object, object)
def __init__(self, vertical, parent=None):
QWidget.__init__(self, parent)
@ -203,6 +205,7 @@ class CoverView(QWidget): # {{{
)
def contextMenuEvent(self, ev):
from calibre.gui2.open_with import populate_menu
cm = QMenu(self)
paste = cm.addAction(_('Paste Cover'))
copy = cm.addAction(_('Copy Cover'))
@ -214,8 +217,28 @@ class CoverView(QWidget): # {{{
paste.triggered.connect(self.paste_from_clipboard)
remove.triggered.connect(self.remove_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())
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):
QApplication.instance().clipboard().setPixmap(self.pixmap)
@ -286,6 +309,7 @@ class BookInfo(QWebView):
compare_format = pyqtSignal(int, object)
copy_link = pyqtSignal(object)
manage_author = pyqtSignal(object)
open_fmt_with = pyqtSignal(int, object, object)
def __init__(self, vertical, parent=None):
QWebView.__init__(self, parent)
@ -405,7 +429,16 @@ class BookInfo(QWebView):
ac.current_fmt = (book_id, fmt)
ac.setText(t)
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:
el = r.linkElement()
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:
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)
files_dropped = pyqtSignal(object, object)
cover_changed = pyqtSignal(object, object)
open_cover_with = pyqtSignal(object, object)
cover_removed = pyqtSignal(object)
view_device_book = pyqtSignal(object)
manage_author = pyqtSignal(object)
open_fmt_with = pyqtSignal(int, object, object)
# Drag 'n drop {{{
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
@ -590,12 +634,14 @@ class BookDetails(QWidget): # {{{
self.cover_view = CoverView(vertical, self)
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._layout.addWidget(self.cover_view)
self.book_info = BookInfo(vertical, self)
self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self.handle_click)
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.restore_format.connect(self.restore_specific_format)
self.book_info.compare_format.connect(self.compare_specific_format)
@ -640,4 +686,3 @@ class BookDetails(QWidget): # {{{
self.show_data(Metadata(_('Unknown')))
# }}}

View File

@ -489,6 +489,10 @@ class LayoutMixin(object): # {{{
self.book_details.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book)
self.book_details.cover_changed.connect(self.bd_cover_changed,
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,
type=Qt.QueuedConnection)
self.book_details.remote_file_dropped.connect(
@ -525,6 +529,18 @@ class LayoutMixin(object): # {{{
if self.cover_flow:
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_):
self.library_view.model().db.remove_cover(id_, commit=True,
notify=False)
@ -570,5 +586,3 @@ class LayoutMixin(object): # {{{
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
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.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.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:
pass
oprefs = JSONConfig('windows_open_with')
run_program
def run_program(entry, path, parent):
raise NotImplementedError()
elif isosx:
pass
oprefs = JSONConfig('osx_open_with')
else:
# Linux find_programs {{{
oprefs = JSONConfig('xdg_open_with')
# XDG find_programs {{{
def parse_localized_key(key):
name, rest = key.partition('[')[0::2]
if not rest:
@ -42,6 +83,7 @@ else:
return
group = None
ans = {}
ans['desktop_file_path'] = path
for line in raw.splitlines():
m = gpat.match(line)
if m is not None:
@ -207,25 +249,61 @@ else:
if comment:
comment += '\n'
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
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):
class ChooseProgram(Dialog): # {{{
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.programs = self.find_error = self.selected_entry = None
self.select_manually = False
@ -258,6 +336,8 @@ class ChooseProgram(Dialog):
l.addWidget(la), l.addWidget(pl)
la.setBuddy(pl)
b = self.bb.addButton(_('&Browse computer for program'), self.bb.ActionRole)
b.clicked.connect(self.manual)
l.addWidget(self.bb)
def sizeHint(self):
@ -291,10 +371,36 @@ class ChooseProgram(Dialog):
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):
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__':
from pprint import pprint
app = QApplication([])
d = ChooseProgram()
d.exec_()
pprint(d.selected_entry)
pprint(choose_program('pdf'))
del app