mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Wire up Open With
This commit is contained in:
parent
d96c632028
commit
1451573bc8
@ -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')))
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -100,7 +100,7 @@ class LibraryViewMixin(object): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class QuickviewSplitter(QSplitter): # {{{
|
||||
class QuickviewSplitter(QSplitter): # {{{
|
||||
|
||||
def __init__(self, parent=None, orientation=Qt.Vertical, qv_widget=None):
|
||||
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.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)
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user