mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Finish up Open With implementation on windows
This commit is contained in:
parent
eec1cfe8a7
commit
aff13a9854
@ -6,9 +6,10 @@ from __future__ import (unicode_literals, division, absolute_import,
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import os, uuid
|
import os, uuid, re
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from future_builtins import map
|
||||||
|
|
||||||
from PyQt5.Qt import (
|
from PyQt5.Qt import (
|
||||||
QApplication, QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt,
|
QApplication, QStackedLayout, QVBoxLayout, QWidget, QLabel, Qt,
|
||||||
@ -17,13 +18,13 @@ from PyQt5.Qt import (
|
|||||||
|
|
||||||
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
|
from calibre.gui2 import error_dialog, choose_files, choose_images, elided_text
|
||||||
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
|
||||||
|
from calibre.utils.icu import numeric_sort_key as sort_key
|
||||||
|
|
||||||
DESC_ROLE = Qt.UserRole
|
ENTRY_ROLE = Qt.UserRole
|
||||||
ENTRY_ROLE = DESC_ROLE + 1
|
|
||||||
|
|
||||||
def pixmap_to_data(pixmap):
|
def pixmap_to_data(pixmap):
|
||||||
ba = QByteArray()
|
ba = QByteArray()
|
||||||
@ -47,11 +48,98 @@ def run_program(entry, path, parent):
|
|||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
def entry_to_icon_text(entry, only_text=False):
|
||||||
|
if only_text:
|
||||||
|
return entry.get('name', entry.get('Name')) or _('Unknown')
|
||||||
|
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.get('name', entry.get('Name')) or _('Unknown')
|
||||||
|
|
||||||
if iswindows:
|
if iswindows:
|
||||||
|
# Windows {{{
|
||||||
|
from calibre.utils.winreg.default_programs import find_programs, friendly_app_name
|
||||||
|
from calibre.gui2 import must_use_qt
|
||||||
|
from win32gui import ExtractIconEx, DestroyIcon
|
||||||
|
from win32process import CreateProcess, STARTUPINFO
|
||||||
|
from win32event import WaitForInputIdle
|
||||||
|
import win32con
|
||||||
|
from PyQt5.Qt import QtWin
|
||||||
oprefs = JSONConfig('windows_open_with')
|
oprefs = JSONConfig('windows_open_with')
|
||||||
run_program
|
|
||||||
|
def entry_sort_key(entry):
|
||||||
|
return sort_key(entry.get('name') or '')
|
||||||
|
|
||||||
|
def load_icon_resource(icon_resource, as_data=False):
|
||||||
|
if not icon_resource:
|
||||||
|
return
|
||||||
|
parts = tuple(filter(None, re.split(r',([-0-9]+$)', icon_resource)))
|
||||||
|
if len(parts) != 2:
|
||||||
|
return
|
||||||
|
module, index = parts
|
||||||
|
large_icons, small_icons = ExtractIconEx(module, int(index), 1)
|
||||||
|
icons = large_icons + small_icons
|
||||||
|
try:
|
||||||
|
if icons:
|
||||||
|
must_use_qt()
|
||||||
|
pixmap = QtWin.fromHICON(icons[0])
|
||||||
|
pixmap = pixmap.scaled(48, 48, transformMode=Qt.SmoothTransformation)
|
||||||
|
if as_data:
|
||||||
|
return pixmap_to_data(pixmap)
|
||||||
|
return QIcon(pixmap)
|
||||||
|
finally:
|
||||||
|
tuple(map(DestroyIcon, icons))
|
||||||
|
|
||||||
|
def finalize_entry(entry):
|
||||||
|
data = load_icon_resource(entry.pop('icon_resource', None), as_data=True)
|
||||||
|
if data:
|
||||||
|
entry['icon_data'] = data
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def entry_to_item(entry, parent):
|
||||||
|
icon = load_icon_resource(entry.get('icon_resource')) or QIcon(I('blank.png'))
|
||||||
|
ans = QListWidgetItem(icon, entry.get('name') or _('Unknown'), parent)
|
||||||
|
ans.setData(ENTRY_ROLE, entry)
|
||||||
|
ans.setToolTip(_('Command line:') + '\n' + entry['cmdline'])
|
||||||
|
|
||||||
|
def choose_manually(filetype, parent):
|
||||||
|
ans = choose_files(
|
||||||
|
parent, 'choose-open-with-program-manually-win',
|
||||||
|
_('Choose a program to open %s files') % filetype.upper(),
|
||||||
|
filters=[(_('Executable files'), ['exe', 'bat', 'com'])], select_only_single_file=True)
|
||||||
|
if ans:
|
||||||
|
ans = os.path.abspath(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)
|
||||||
|
qans = ans.replace('"', r'\"')
|
||||||
|
name = friendly_app_name(exe=ans) or os.path.splitext(os.path.basename(ans))[0]
|
||||||
|
return {'cmdline':'"%s" "%%1"' % qans, 'name':name, 'icon_resource':ans + ',0'}
|
||||||
|
|
||||||
|
def entry_to_cmdline(entry, path):
|
||||||
|
cmdline = entry['cmdline']
|
||||||
|
qpath = path.replace('"', r'\"')
|
||||||
|
return cmdline.replace('%1', qpath)
|
||||||
|
|
||||||
|
del run_program
|
||||||
def run_program(entry, path, parent):
|
def run_program(entry, path, parent):
|
||||||
raise NotImplementedError()
|
cmdline = entry_to_cmdline(entry, path)
|
||||||
|
print('Running Open With commandline:', repr(entry['cmdline']), ' |==> ', repr(cmdline))
|
||||||
|
try:
|
||||||
|
process_handle, thread_handle, process_id, thread_id = CreateProcess(
|
||||||
|
None, cmdline, None, None, False, win32con.CREATE_DEFAULT_ERROR_MODE | win32con.CREATE_NEW_PROCESS_GROUP | win32con.DETACHED_PROCESS,
|
||||||
|
None, None, STARTUPINFO())
|
||||||
|
WaitForInputIdle(process_handle, 2000)
|
||||||
|
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)))
|
||||||
|
# }}}
|
||||||
|
|
||||||
elif isosx:
|
elif isosx:
|
||||||
oprefs = JSONConfig('osx_open_with')
|
oprefs = JSONConfig('osx_open_with')
|
||||||
@ -60,22 +148,9 @@ else:
|
|||||||
oprefs = JSONConfig('xdg_open_with')
|
oprefs = JSONConfig('xdg_open_with')
|
||||||
from calibre.utils.open_with.linux import entry_to_cmdline, find_programs, entry_sort_key
|
from calibre.utils.open_with.linux import entry_to_cmdline, find_programs, entry_sort_key
|
||||||
|
|
||||||
def entry_to_icon_text(entry, only_text=False):
|
|
||||||
if only_text:
|
|
||||||
return entry['Name']
|
|
||||||
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_item(entry, parent):
|
def entry_to_item(entry, parent):
|
||||||
icon_path = entry.get('Icon') or I('blank.png')
|
icon_path = entry.get('Icon') or I('blank.png')
|
||||||
ans = QListWidgetItem(QIcon(icon_path), entry.get('Name') or _('Unknown'), parent)
|
ans = QListWidgetItem(QIcon(icon_path), entry.get('Name') or _('Unknown'), parent)
|
||||||
ans.setData(DESC_ROLE, entry.get('Comment') or '')
|
|
||||||
ans.setData(ENTRY_ROLE, entry)
|
ans.setData(ENTRY_ROLE, entry)
|
||||||
comment = (entry.get('Comment') or '')
|
comment = (entry.get('Comment') or '')
|
||||||
if comment:
|
if comment:
|
||||||
@ -203,6 +278,7 @@ def populate_menu(menu, receiver, file_type):
|
|||||||
file_type = file_type.lower()
|
file_type = file_type.lower()
|
||||||
for entry in oprefs['entries'].get(file_type, ()):
|
for entry in oprefs['entries'].get(file_type, ()):
|
||||||
icon, text = entry_to_icon_text(entry)
|
icon, text = entry_to_icon_text(entry)
|
||||||
|
text = elided_text(text, pos='right')
|
||||||
sa = registered_shortcuts.get(entry['uuid'])
|
sa = registered_shortcuts.get(entry['uuid'])
|
||||||
if sa is not None:
|
if sa is not None:
|
||||||
text += '\t' + sa.shortcut().toString(QKeySequence.NativeText)
|
text += '\t' + sa.shortcut().toString(QKeySequence.NativeText)
|
||||||
|
@ -225,10 +225,14 @@ def split_commandline(commandline):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def friendly_app_name(prog_id=None, exe=None):
|
def friendly_app_name(prog_id=None, exe=None):
|
||||||
from win32com.shell import shell, shellcon
|
try:
|
||||||
a = shell.AssocCreate()
|
from win32com.shell import shell, shellcon
|
||||||
a.Init((shellcon.ASSOCF_INIT_BYEXENAME if exe else 0), exe or prog_id)
|
a = shell.AssocCreate()
|
||||||
return a.GetString(shellcon.ASSOCF_REMAPRUNDLL, shellcon.ASSOCSTR_FRIENDLYAPPNAME)
|
a.Init((shellcon.ASSOCF_INIT_BYEXENAME if exe else 0), exe or prog_id)
|
||||||
|
return a.GetString(shellcon.ASSOCF_REMAPRUNDLL, shellcon.ASSOCSTR_FRIENDLYAPPNAME)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def find_programs(extensions):
|
def find_programs(extensions):
|
||||||
extensions = frozenset(extensions)
|
extensions = frozenset(extensions)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user