mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
2b92479e13
BIN
resources/images/news/kompiutierra.png
Normal file
BIN
resources/images/news/kompiutierra.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 654 B |
BIN
resources/images/news/rbc_ru.png
Normal file
BIN
resources/images/news/rbc_ru.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 371 B |
@ -4,6 +4,7 @@
|
|||||||
# #
|
# #
|
||||||
# #
|
# #
|
||||||
# copyright 2002 Paul Henry Tremblay #
|
# copyright 2002 Paul Henry Tremblay #
|
||||||
|
# Copyright 2011 Kovid Goyal
|
||||||
# #
|
# #
|
||||||
# This program is distributed in the hope that it will be useful, #
|
# This program is distributed in the hope that it will be useful, #
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||||
@ -293,6 +294,23 @@
|
|||||||
<h3>Annotation</h3>
|
<h3>Annotation</h3>
|
||||||
<xsl:apply-templates/>
|
<xsl:apply-templates/>
|
||||||
</xsl:template>
|
</xsl:template>
|
||||||
|
<!-- tables -->
|
||||||
|
<xsl:template match="fb:table">
|
||||||
|
<table>
|
||||||
|
<xsl:apply-templates/>
|
||||||
|
</table>
|
||||||
|
</xsl:template>
|
||||||
|
<xsl:template match="fb:tr">
|
||||||
|
<tr><xsl:apply-templates/></tr>
|
||||||
|
</xsl:template>
|
||||||
|
<xsl:template match="fb:td">
|
||||||
|
<xsl:element name="td">
|
||||||
|
<xsl:if test="@align">
|
||||||
|
<xsl:attribute name="align"><xsl:value-of select="@align"/></xsl:attribute>
|
||||||
|
</xsl:if>
|
||||||
|
<xsl:apply-templates/>
|
||||||
|
</xsl:element>
|
||||||
|
</xsl:template>
|
||||||
<!-- epigraph -->
|
<!-- epigraph -->
|
||||||
<xsl:template match="fb:epigraph">
|
<xsl:template match="fb:epigraph">
|
||||||
<blockquote class="epigraph">
|
<blockquote class="epigraph">
|
||||||
|
@ -61,8 +61,9 @@ def osx_version():
|
|||||||
if m:
|
if m:
|
||||||
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
||||||
|
|
||||||
|
|
||||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||||
|
_filename_sanitize_unicode = frozenset([u'\\', u'|', u'?', u'*', u'<',
|
||||||
|
u'"', u':', u'>', u'+', u'/'] + list(map(unichr, xrange(32))))
|
||||||
|
|
||||||
def sanitize_file_name(name, substitute='_', as_unicode=False):
|
def sanitize_file_name(name, substitute='_', as_unicode=False):
|
||||||
'''
|
'''
|
||||||
@ -83,8 +84,35 @@ def sanitize_file_name(name, substitute='_', as_unicode=False):
|
|||||||
one = one.decode(filesystem_encoding)
|
one = one.decode(filesystem_encoding)
|
||||||
one = one.replace('..', substitute)
|
one = one.replace('..', substitute)
|
||||||
# Windows doesn't like path components that end with a period
|
# Windows doesn't like path components that end with a period
|
||||||
if one.endswith('.'):
|
if one and one[-1] in ('.', ' '):
|
||||||
one = one[:-1]+'_'
|
one = one[:-1]+'_'
|
||||||
|
# Names starting with a period are hidden on Unix
|
||||||
|
if one.startswith('.'):
|
||||||
|
one = '_' + one[1:]
|
||||||
|
return one
|
||||||
|
|
||||||
|
def sanitize_file_name_unicode(name, substitute='_'):
|
||||||
|
'''
|
||||||
|
Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
|
||||||
|
The set of invalid characters is the union of the invalid characters in Windows,
|
||||||
|
OS X and Linux. Also removes leading and trailing whitespace.
|
||||||
|
**WARNING:** This function also replaces path separators, so only pass file names
|
||||||
|
and not full paths to it.
|
||||||
|
'''
|
||||||
|
if not isinstance(name, unicode):
|
||||||
|
return sanitize_file_name(name, substitute=substitute, as_unicode=True)
|
||||||
|
chars = [substitute if c in _filename_sanitize_unicode else c for c in
|
||||||
|
name]
|
||||||
|
one = u''.join(chars)
|
||||||
|
one = re.sub(r'\s', ' ', one).strip()
|
||||||
|
one = re.sub(r'^\.+$', '_', one)
|
||||||
|
one = one.replace('..', substitute)
|
||||||
|
# Windows doesn't like path components that end with a period or space
|
||||||
|
if one and one[-1] in ('.', ' '):
|
||||||
|
one = one[:-1]+'_'
|
||||||
|
# Names starting with a period are hidden on Unix
|
||||||
|
if one.startswith('.'):
|
||||||
|
one = '_' + one[1:]
|
||||||
return one
|
return one
|
||||||
|
|
||||||
|
|
||||||
|
@ -204,15 +204,29 @@ class AddAction(InterfaceAction):
|
|||||||
to_device = self.gui.stack.currentIndex() != 0
|
to_device = self.gui.stack.currentIndex() != 0
|
||||||
self._add_books(paths, to_device)
|
self._add_books(paths, to_device)
|
||||||
|
|
||||||
def files_dropped_on_book(self, event, paths):
|
def remote_file_dropped_on_book(self, url, fname):
|
||||||
|
if self.gui.current_view() is not self.gui.library_view:
|
||||||
|
return
|
||||||
|
db = self.gui.library_view.model().db
|
||||||
|
current_idx = self.gui.library_view.currentIndex()
|
||||||
|
if not current_idx.isValid(): return
|
||||||
|
cid = db.id(current_idx.row())
|
||||||
|
from calibre.gui2.dnd import DownloadDialog
|
||||||
|
d = DownloadDialog(url, fname, self.gui)
|
||||||
|
d.start_download()
|
||||||
|
if d.err is None:
|
||||||
|
self.files_dropped_on_book(None, [d.fpath], cid=cid)
|
||||||
|
|
||||||
|
def files_dropped_on_book(self, event, paths, cid=None):
|
||||||
accept = False
|
accept = False
|
||||||
if self.gui.current_view() is not self.gui.library_view:
|
if self.gui.current_view() is not self.gui.library_view:
|
||||||
return
|
return
|
||||||
db = self.gui.library_view.model().db
|
db = self.gui.library_view.model().db
|
||||||
cover_changed = False
|
cover_changed = False
|
||||||
current_idx = self.gui.library_view.currentIndex()
|
current_idx = self.gui.library_view.currentIndex()
|
||||||
|
if cid is None:
|
||||||
if not current_idx.isValid(): return
|
if not current_idx.isValid(): return
|
||||||
cid = db.id(current_idx.row())
|
cid = db.id(current_idx.row()) if cid is None else cid
|
||||||
for path in paths:
|
for path in paths:
|
||||||
ext = os.path.splitext(path)[1].lower()
|
ext = os.path.splitext(path)[1].lower()
|
||||||
if ext:
|
if ext:
|
||||||
@ -227,8 +241,9 @@ class AddAction(InterfaceAction):
|
|||||||
elif ext in BOOK_EXTENSIONS:
|
elif ext in BOOK_EXTENSIONS:
|
||||||
db.add_format_with_hooks(cid, ext, path, index_is_id=True)
|
db.add_format_with_hooks(cid, ext, path, index_is_id=True)
|
||||||
accept = True
|
accept = True
|
||||||
if accept:
|
if accept and event is not None:
|
||||||
event.accept()
|
event.accept()
|
||||||
|
if current_idx.isValid():
|
||||||
self.gui.library_view.model().current_changed(current_idx, current_idx)
|
self.gui.library_view.model().current_changed(current_idx, current_idx)
|
||||||
if cover_changed:
|
if cover_changed:
|
||||||
if self.gui.cover_flow:
|
if self.gui.cover_flow:
|
||||||
|
@ -5,7 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, collections, sys
|
import collections, sys
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
|
|
||||||
from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \
|
from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \
|
||||||
@ -14,7 +14,8 @@ from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \
|
|||||||
from PyQt4.QtWebKit import QWebView
|
from PyQt4.QtWebKit import QWebView
|
||||||
|
|
||||||
from calibre import fit_image, prepare_string_for_xml
|
from calibre import fit_image, prepare_string_for_xml
|
||||||
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \
|
||||||
|
IMAGE_EXTENSIONS, dnd_has_extension
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.constants import preferred_encoding
|
from calibre.constants import preferred_encoding
|
||||||
from calibre.library.comments import comments_to_html
|
from calibre.library.comments import comments_to_html
|
||||||
@ -165,7 +166,8 @@ class CoverView(QWidget): # {{{
|
|||||||
def copy_to_clipboard(self):
|
def copy_to_clipboard(self):
|
||||||
QApplication.instance().clipboard().setPixmap(self.pixmap)
|
QApplication.instance().clipboard().setPixmap(self.pixmap)
|
||||||
|
|
||||||
def paste_from_clipboard(self):
|
def paste_from_clipboard(self, pmap=None):
|
||||||
|
if not isinstance(pmap, QPixmap):
|
||||||
cb = QApplication.instance().clipboard()
|
cb = QApplication.instance().clipboard()
|
||||||
pmap = cb.pixmap()
|
pmap = cb.pixmap()
|
||||||
if pmap.isNull() and cb.supportsSelection():
|
if pmap.isNull() and cb.supportsSelection():
|
||||||
@ -226,6 +228,7 @@ class BookInfo(QWebView):
|
|||||||
self._link_clicked = False
|
self._link_clicked = False
|
||||||
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
|
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
|
||||||
palette = self.palette()
|
palette = self.palette()
|
||||||
|
self.setAcceptDrops(False)
|
||||||
palette.setBrush(QPalette.Base, Qt.transparent)
|
palette.setBrush(QPalette.Base, Qt.transparent)
|
||||||
self.page().setPalette(palette)
|
self.page().setPalette(palette)
|
||||||
|
|
||||||
@ -388,36 +391,50 @@ class BookDetails(QWidget): # {{{
|
|||||||
show_book_info = pyqtSignal()
|
show_book_info = pyqtSignal()
|
||||||
open_containing_folder = pyqtSignal(int)
|
open_containing_folder = pyqtSignal(int)
|
||||||
view_specific_format = pyqtSignal(int, object)
|
view_specific_format = pyqtSignal(int, object)
|
||||||
|
remote_file_dropped = pyqtSignal(object, object)
|
||||||
# Drag 'n drop {{{
|
|
||||||
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
|
|
||||||
files_dropped = pyqtSignal(object, object)
|
files_dropped = pyqtSignal(object, object)
|
||||||
cover_changed = pyqtSignal(object, object)
|
cover_changed = pyqtSignal(object, object)
|
||||||
|
|
||||||
# application/x-moz-file-promise-url
|
# Drag 'n drop {{{
|
||||||
@classmethod
|
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
|
||||||
def paths_from_event(cls, event):
|
|
||||||
'''
|
|
||||||
Accept a drop event and return a list of paths that can be read from
|
|
||||||
and represent files with extensions.
|
|
||||||
'''
|
|
||||||
if event.mimeData().hasFormat('text/uri-list'):
|
|
||||||
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
|
||||||
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
|
|
||||||
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
|
|
||||||
|
|
||||||
def dragEnterEvent(self, event):
|
def dragEnterEvent(self, event):
|
||||||
if int(event.possibleActions() & Qt.CopyAction) + \
|
md = event.mimeData()
|
||||||
int(event.possibleActions() & Qt.MoveAction) == 0:
|
if dnd_has_extension(md, self.DROPABBLE_EXTENSIONS) or \
|
||||||
return
|
dnd_has_image(md):
|
||||||
paths = self.paths_from_event(event)
|
|
||||||
if paths:
|
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
|
|
||||||
def dropEvent(self, event):
|
def dropEvent(self, event):
|
||||||
paths = self.paths_from_event(event)
|
|
||||||
event.setDropAction(Qt.CopyAction)
|
event.setDropAction(Qt.CopyAction)
|
||||||
self.files_dropped.emit(event, paths)
|
md = event.mimeData()
|
||||||
|
|
||||||
|
x, y = dnd_get_image(md)
|
||||||
|
if x is not None:
|
||||||
|
# We have an image, set cover
|
||||||
|
event.accept()
|
||||||
|
if y is None:
|
||||||
|
# Local image
|
||||||
|
self.cover_view.paste_from_clipboard(x)
|
||||||
|
else:
|
||||||
|
self.remote_file_dropped.emit(x, y)
|
||||||
|
# We do not support setting cover *and* adding formats for
|
||||||
|
# a remote drop, anyway, so return
|
||||||
|
return
|
||||||
|
|
||||||
|
# Now look for ebook files
|
||||||
|
urls, filenames = dnd_get_files(md, BOOK_EXTENSIONS)
|
||||||
|
if not urls:
|
||||||
|
# Nothing found
|
||||||
|
return
|
||||||
|
|
||||||
|
if not filenames:
|
||||||
|
# Local files
|
||||||
|
self.files_dropped.emit(event, urls)
|
||||||
|
else:
|
||||||
|
# Remote files, use the first file
|
||||||
|
self.remote_file_dropped.emit(urls[0], filenames[0])
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
def dragMoveEvent(self, event):
|
def dragMoveEvent(self, event):
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
|
@ -43,6 +43,9 @@
|
|||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="sizeAdjustPolicy">
|
||||||
|
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
||||||
|
</property>
|
||||||
<property name="minimumContentsLength">
|
<property name="minimumContentsLength">
|
||||||
<number>30</number>
|
<number>30</number>
|
||||||
</property>
|
</property>
|
||||||
|
325
src/calibre/gui2/dnd.py
Normal file
325
src/calibre/gui2/dnd.py
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import posixpath, os, urllib, re
|
||||||
|
from urlparse import urlparse, urlunparse
|
||||||
|
from threading import Thread
|
||||||
|
from Queue import Queue, Empty
|
||||||
|
|
||||||
|
from PyQt4.Qt import QPixmap, Qt, QDialog, QLabel, QVBoxLayout, \
|
||||||
|
QDialogButtonBox, QProgressBar, QTimer
|
||||||
|
|
||||||
|
from calibre.constants import DEBUG, iswindows
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre import browser, as_unicode, prints
|
||||||
|
from calibre.gui2 import error_dialog
|
||||||
|
|
||||||
|
IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'gif', 'png', 'bmp']
|
||||||
|
|
||||||
|
class Worker(Thread): # {{{
|
||||||
|
|
||||||
|
def __init__(self, url, fpath, rq):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.url, self.fpath = url, fpath
|
||||||
|
self.daemon = True
|
||||||
|
self.rq = rq
|
||||||
|
self.err = self.tb = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
br = browser()
|
||||||
|
br.retrieve(self.url, self.fpath, self.callback)
|
||||||
|
except Exception, e:
|
||||||
|
self.err = as_unicode(e)
|
||||||
|
import traceback
|
||||||
|
self.tb = traceback.format_exc()
|
||||||
|
|
||||||
|
def callback(self, a, b, c):
|
||||||
|
self.rq.put((a, b, c))
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class DownloadDialog(QDialog): # {{{
|
||||||
|
|
||||||
|
def __init__(self, url, fname, parent):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.setWindowTitle(_('Download %s')%fname)
|
||||||
|
self.l = QVBoxLayout(self)
|
||||||
|
self.purl = urlparse(url)
|
||||||
|
self.msg = QLabel(_('Downloading <b>%s</b> from %s')%(fname,
|
||||||
|
self.purl.netloc))
|
||||||
|
self.msg.setWordWrap(True)
|
||||||
|
self.l.addWidget(self.msg)
|
||||||
|
self.pb = QProgressBar(self)
|
||||||
|
self.pb.setMinimum(0)
|
||||||
|
self.pb.setMaximum(0)
|
||||||
|
self.l.addWidget(self.pb)
|
||||||
|
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel, Qt.Horizontal, self)
|
||||||
|
self.l.addWidget(self.bb)
|
||||||
|
self.bb.rejected.connect(self.reject)
|
||||||
|
sz = self.sizeHint()
|
||||||
|
self.resize(max(sz.width(), 400), sz.height())
|
||||||
|
|
||||||
|
fpath = PersistentTemporaryFile(os.path.splitext(fname)[1])
|
||||||
|
fpath.close()
|
||||||
|
self.fpath = fpath.name
|
||||||
|
|
||||||
|
self.worker = Worker(url, self.fpath, Queue())
|
||||||
|
self.rejected = False
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
self.rejected = True
|
||||||
|
QDialog.reject(self)
|
||||||
|
|
||||||
|
def start_download(self):
|
||||||
|
self.worker.start()
|
||||||
|
QTimer.singleShot(50, self.update)
|
||||||
|
self.exec_()
|
||||||
|
if self.worker.err is not None:
|
||||||
|
error_dialog(self.parent(), _('Download failed'),
|
||||||
|
_('Failed to download from %r with error: %s')%(
|
||||||
|
self.worker.url, self.worker.err),
|
||||||
|
det_msg=self.worker.tb, show=True)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
if self.rejected:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
progress = self.worker.rq.get_nowait()
|
||||||
|
except Empty:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.update_pb(progress)
|
||||||
|
|
||||||
|
if not self.worker.is_alive():
|
||||||
|
return self.accept()
|
||||||
|
QTimer.singleShot(50, self.update)
|
||||||
|
|
||||||
|
def update_pb(self, progress):
|
||||||
|
transferred, block_size, total = progress
|
||||||
|
if total == -1:
|
||||||
|
self.pb.setMaximum(0)
|
||||||
|
self.pb.setMinimum(0)
|
||||||
|
self.pb.setValue(0)
|
||||||
|
else:
|
||||||
|
so_far = transferred * block_size
|
||||||
|
self.pb.setMaximum(max(total, so_far))
|
||||||
|
self.pb.setValue(so_far)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def err(self):
|
||||||
|
return self.worker.err
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def dnd_has_image(md):
|
||||||
|
return md.hasImage()
|
||||||
|
|
||||||
|
def data_as_string(f, md):
|
||||||
|
raw = bytes(md.data(f))
|
||||||
|
if '/x-moz' in f:
|
||||||
|
try:
|
||||||
|
raw = raw.decode('utf-16')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return raw
|
||||||
|
|
||||||
|
def dnd_has_extension(md, extensions):
|
||||||
|
if DEBUG:
|
||||||
|
prints('Debugging DND event')
|
||||||
|
for f in md.formats():
|
||||||
|
f = unicode(f)
|
||||||
|
prints(f, repr(data_as_string(f, md))[:300], '\n')
|
||||||
|
print ()
|
||||||
|
if has_firefox_ext(md, extensions):
|
||||||
|
return True
|
||||||
|
if md.hasUrls():
|
||||||
|
urls = [unicode(u.toString()) for u in
|
||||||
|
md.urls()]
|
||||||
|
purls = [urlparse(u) for u in urls]
|
||||||
|
if DEBUG:
|
||||||
|
prints('URLS:', urls)
|
||||||
|
prints('Paths:', [u2p(x) for x in purls])
|
||||||
|
|
||||||
|
exts = frozenset([posixpath.splitext(u.path)[1][1:].lower() for u in
|
||||||
|
purls])
|
||||||
|
return bool(exts.intersection(frozenset(extensions)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def u2p(url):
|
||||||
|
path = url.path
|
||||||
|
if iswindows:
|
||||||
|
if path.startswith('/'):
|
||||||
|
path = path[1:]
|
||||||
|
ans = path.replace('/', os.sep)
|
||||||
|
if os.path.exists(ans):
|
||||||
|
return ans
|
||||||
|
# Try unquoting the URL
|
||||||
|
return urllib.unquote(ans)
|
||||||
|
|
||||||
|
def dnd_get_image(md, image_exts=IMAGE_EXTENSIONS):
|
||||||
|
'''
|
||||||
|
Get the image in the QMimeData object md.
|
||||||
|
|
||||||
|
:return: None, None if no image is found
|
||||||
|
QPixmap, None if an image is found, the pixmap is guaranteed not
|
||||||
|
null
|
||||||
|
url, filename if a URL that points to an image is found
|
||||||
|
'''
|
||||||
|
if dnd_has_image(md):
|
||||||
|
for x in md.formats():
|
||||||
|
x = unicode(x)
|
||||||
|
if x.startswith('image/'):
|
||||||
|
cdata = bytes(md.data(x))
|
||||||
|
pmap = QPixmap()
|
||||||
|
pmap.loadFromData(cdata)
|
||||||
|
if not pmap.isNull():
|
||||||
|
return pmap, None
|
||||||
|
break
|
||||||
|
|
||||||
|
# No image, look for a URL pointing to an image
|
||||||
|
if md.hasUrls():
|
||||||
|
urls = [unicode(u.toString()) for u in
|
||||||
|
md.urls()]
|
||||||
|
purls = [urlparse(u) for u in urls]
|
||||||
|
# First look for a local file
|
||||||
|
images = [u2p(x) for x in purls if x.scheme in ('', 'file') and
|
||||||
|
posixpath.splitext(urllib.unquote(x.path))[1][1:].lower() in
|
||||||
|
image_exts]
|
||||||
|
images = [x for x in images if os.path.exists(x)]
|
||||||
|
p = QPixmap()
|
||||||
|
for path in images:
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
p.loadFromData(f.read())
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if not p.isNull():
|
||||||
|
return p, None
|
||||||
|
|
||||||
|
# No local images, look for remote ones
|
||||||
|
|
||||||
|
# First, see if this is from Firefox
|
||||||
|
rurl, fname = get_firefox_rurl(md, image_exts)
|
||||||
|
|
||||||
|
if rurl and fname:
|
||||||
|
return rurl, fname
|
||||||
|
# Look through all remaining URLs
|
||||||
|
remote_urls = [x for x in purls if x.scheme in ('http', 'https',
|
||||||
|
'ftp') and posixpath.splitext(x.path)[1][1:].lower() in image_exts]
|
||||||
|
if remote_urls:
|
||||||
|
rurl = remote_urls[0]
|
||||||
|
fname = posixpath.basename(urllib.unquote(rurl.path))
|
||||||
|
return urlunparse(rurl), fname
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def dnd_get_files(md, exts):
|
||||||
|
'''
|
||||||
|
Get the file in the QMimeData object md with an extension that is one of
|
||||||
|
the extensions in exts.
|
||||||
|
|
||||||
|
:return: None, None if no file is found
|
||||||
|
[paths], None if a local file is found
|
||||||
|
[urls], [filenames] if URLs that point to a files are found
|
||||||
|
'''
|
||||||
|
# Look for a URL pointing to a file
|
||||||
|
if md.hasUrls():
|
||||||
|
urls = [unicode(u.toString()) for u in
|
||||||
|
md.urls()]
|
||||||
|
purls = [urlparse(u) for u in urls]
|
||||||
|
# First look for a local file
|
||||||
|
local_files = [u2p(x) for x in purls if x.scheme in ('', 'file') and
|
||||||
|
posixpath.splitext(urllib.unquote(x.path))[1][1:].lower() in
|
||||||
|
exts]
|
||||||
|
local_files = [x for x in local_files if os.path.exists(x)]
|
||||||
|
if local_files:
|
||||||
|
return local_files, None
|
||||||
|
|
||||||
|
# No local files, look for remote ones
|
||||||
|
|
||||||
|
# First, see if this is from Firefox
|
||||||
|
rurl, fname = get_firefox_rurl(md, exts)
|
||||||
|
if rurl and fname:
|
||||||
|
return [rurl], [fname]
|
||||||
|
|
||||||
|
# Look through all remaining URLs
|
||||||
|
remote_urls = [x for x in purls if x.scheme in ('http', 'https',
|
||||||
|
'ftp') and posixpath.splitext(x.path)[1][1:].lower() in exts]
|
||||||
|
if remote_urls:
|
||||||
|
filenames = [posixpath.basename(urllib.unquote(rurl.path)) for rurl in
|
||||||
|
remote_urls]
|
||||||
|
return [urlunparse(x) for x in remote_urls], filenames
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def _get_firefox_pair(md, exts, url, fname):
|
||||||
|
url = bytes(md.data(url)).decode('utf-16')
|
||||||
|
fname = bytes(md.data(fname)).decode('utf-16')
|
||||||
|
while url.endswith('\x00'):
|
||||||
|
url = url[:-1]
|
||||||
|
while fname.endswith('\x00'):
|
||||||
|
fname = fname[:-1]
|
||||||
|
if not url or not fname:
|
||||||
|
return None, None
|
||||||
|
ext = posixpath.splitext(fname)[1][1:].lower()
|
||||||
|
# Weird firefox bug on linux
|
||||||
|
ext = {'jpe':'jpg', 'epu':'epub', 'mob':'mobi'}.get(ext, ext)
|
||||||
|
fname = os.path.splitext(fname)[0] + '.' + ext
|
||||||
|
if DEBUG:
|
||||||
|
prints('Firefox file promise:', url, fname)
|
||||||
|
if ext not in exts:
|
||||||
|
fname = url = None
|
||||||
|
return url, fname
|
||||||
|
|
||||||
|
|
||||||
|
def get_firefox_rurl(md, exts):
|
||||||
|
formats = frozenset([unicode(x) for x in md.formats()])
|
||||||
|
url = fname = None
|
||||||
|
if 'application/x-moz-file-promise-url' in formats and \
|
||||||
|
'application/x-moz-file-promise-dest-filename' in formats:
|
||||||
|
try:
|
||||||
|
url, fname = _get_firefox_pair(md, exts,
|
||||||
|
'application/x-moz-file-promise-url',
|
||||||
|
'application/x-moz-file-promise-dest-filename')
|
||||||
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
if url is None and 'text/x-moz-url-data' in formats and \
|
||||||
|
'text/x-moz-url-desc' in formats:
|
||||||
|
try:
|
||||||
|
url, fname = _get_firefox_pair(md, exts,
|
||||||
|
'text/x-moz-url-data', 'text/x-moz-url-desc')
|
||||||
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if url is None and '_NETSCAPE_URL' in formats:
|
||||||
|
try:
|
||||||
|
raw = bytes(md.data('_NETSCAPE_URL'))
|
||||||
|
raw = raw.decode('utf-8')
|
||||||
|
lines = raw.splitlines()
|
||||||
|
if len(lines) > 1 and re.match(r'[a-z]+://', lines[1]) is None:
|
||||||
|
url, fname = lines[:2]
|
||||||
|
ext = posixpath.splitext(fname)[1][1:].lower()
|
||||||
|
if ext not in exts:
|
||||||
|
fname = url = None
|
||||||
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
if DEBUG:
|
||||||
|
prints('Firefox rurl:', url, fname)
|
||||||
|
return url, fname
|
||||||
|
|
||||||
|
def has_firefox_ext(md, exts):
|
||||||
|
return bool(get_firefox_rurl(md, exts)[0])
|
||||||
|
|
@ -264,6 +264,9 @@ 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.remote_file_dropped.connect(
|
||||||
|
self.iactions['Add Books'].remote_file_dropped_on_book,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id)
|
self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id)
|
||||||
self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id)
|
self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id)
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
'''
|
'''
|
||||||
Miscellaneous widgets used in the GUI
|
Miscellaneous widgets used in the GUI
|
||||||
'''
|
'''
|
||||||
import re, os, traceback
|
import re, traceback
|
||||||
|
|
||||||
from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
|
from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
|
||||||
QListWidgetItem, QTextCharFormat, QApplication, \
|
QListWidgetItem, QTextCharFormat, QApplication, \
|
||||||
@ -22,6 +22,8 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
|||||||
from calibre.ebooks.metadata.meta import metadata_from_filename
|
from calibre.ebooks.metadata.meta import metadata_from_filename
|
||||||
from calibre.utils.config import prefs, XMLConfig, tweaks
|
from calibre.utils.config import prefs, XMLConfig, tweaks
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator
|
||||||
|
from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \
|
||||||
|
IMAGE_EXTENSIONS, dnd_has_extension, DownloadDialog
|
||||||
|
|
||||||
history = XMLConfig('history')
|
history = XMLConfig('history')
|
||||||
|
|
||||||
@ -141,36 +143,35 @@ class FilenamePattern(QWidget, Ui_Form):
|
|||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
|
||||||
IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'gif', 'png', 'bmp']
|
|
||||||
|
|
||||||
class FormatList(QListWidget):
|
class FormatList(QListWidget):
|
||||||
DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS
|
DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS
|
||||||
formats_dropped = pyqtSignal(object, object)
|
formats_dropped = pyqtSignal(object, object)
|
||||||
delete_format = pyqtSignal()
|
delete_format = pyqtSignal()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def paths_from_event(cls, event):
|
|
||||||
'''
|
|
||||||
Accept a drop event and return a list of paths that can be read from
|
|
||||||
and represent files with extensions.
|
|
||||||
'''
|
|
||||||
if event.mimeData().hasFormat('text/uri-list'):
|
|
||||||
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
|
||||||
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
|
|
||||||
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
|
|
||||||
|
|
||||||
def dragEnterEvent(self, event):
|
def dragEnterEvent(self, event):
|
||||||
if int(event.possibleActions() & Qt.CopyAction) + \
|
md = event.mimeData()
|
||||||
int(event.possibleActions() & Qt.MoveAction) == 0:
|
if dnd_has_extension(md, self.DROPABBLE_EXTENSIONS):
|
||||||
return
|
|
||||||
paths = self.paths_from_event(event)
|
|
||||||
if paths:
|
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
|
|
||||||
def dropEvent(self, event):
|
def dropEvent(self, event):
|
||||||
paths = self.paths_from_event(event)
|
|
||||||
event.setDropAction(Qt.CopyAction)
|
event.setDropAction(Qt.CopyAction)
|
||||||
self.formats_dropped.emit(event, paths)
|
md = event.mimeData()
|
||||||
|
# Now look for ebook files
|
||||||
|
urls, filenames = dnd_get_files(md, self.DROPABBLE_EXTENSIONS)
|
||||||
|
if not urls:
|
||||||
|
# Nothing found
|
||||||
|
return
|
||||||
|
|
||||||
|
if not filenames:
|
||||||
|
# Local files
|
||||||
|
self.formats_dropped.emit(event, urls)
|
||||||
|
else:
|
||||||
|
# Remote files, use the first file
|
||||||
|
d = DownloadDialog(urls[0], filenames[0], self)
|
||||||
|
d.start_download()
|
||||||
|
if d.err is None:
|
||||||
|
self.formats_dropped.emit(event, [d.fpath])
|
||||||
|
|
||||||
|
|
||||||
def dragMoveEvent(self, event):
|
def dragMoveEvent(self, event):
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
@ -183,7 +184,7 @@ class FormatList(QListWidget):
|
|||||||
|
|
||||||
class ImageDropMixin(object): # {{{
|
class ImageDropMixin(object): # {{{
|
||||||
'''
|
'''
|
||||||
Adds support for dropping images onto widgets and a contect menu for
|
Adds support for dropping images onto widgets and a context menu for
|
||||||
copy/pasting images.
|
copy/pasting images.
|
||||||
'''
|
'''
|
||||||
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS
|
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS
|
||||||
@ -191,39 +192,36 @@ class ImageDropMixin(object): # {{{
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def paths_from_event(cls, event):
|
|
||||||
'''
|
|
||||||
Accept a drop event and return a list of paths that can be read from
|
|
||||||
and represent files with extensions.
|
|
||||||
'''
|
|
||||||
if event.mimeData().hasFormat('text/uri-list'):
|
|
||||||
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
|
||||||
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
|
|
||||||
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
|
|
||||||
|
|
||||||
def dragEnterEvent(self, event):
|
def dragEnterEvent(self, event):
|
||||||
if int(event.possibleActions() & Qt.CopyAction) + \
|
md = event.mimeData()
|
||||||
int(event.possibleActions() & Qt.MoveAction) == 0:
|
if dnd_has_extension(md, self.DROPABBLE_EXTENSIONS) or \
|
||||||
return
|
dnd_has_image(md):
|
||||||
paths = self.paths_from_event(event)
|
|
||||||
if paths:
|
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
|
|
||||||
def dropEvent(self, event):
|
def dropEvent(self, event):
|
||||||
paths = self.paths_from_event(event)
|
|
||||||
event.setDropAction(Qt.CopyAction)
|
event.setDropAction(Qt.CopyAction)
|
||||||
for path in paths:
|
md = event.mimeData()
|
||||||
pmap = QPixmap()
|
|
||||||
pmap.load(path)
|
|
||||||
if not pmap.isNull():
|
|
||||||
self.handle_image_drop(path, pmap)
|
|
||||||
event.accept()
|
|
||||||
break
|
|
||||||
|
|
||||||
def handle_image_drop(self, path, pmap):
|
x, y = dnd_get_image(md)
|
||||||
|
if x is not None:
|
||||||
|
# We have an image, set cover
|
||||||
|
event.accept()
|
||||||
|
if y is None:
|
||||||
|
# Local image
|
||||||
|
self.handle_image_drop(x)
|
||||||
|
else:
|
||||||
|
# Remote files, use the first file
|
||||||
|
d = DownloadDialog(x, y, self)
|
||||||
|
d.start_download()
|
||||||
|
if d.err is None:
|
||||||
|
pmap = QPixmap()
|
||||||
|
pmap.loadFromData(open(d.fpath, 'rb').read())
|
||||||
|
if not pmap.isNull():
|
||||||
|
self.handle_image_drop(pmap)
|
||||||
|
|
||||||
|
def handle_image_drop(self, pmap):
|
||||||
self.set_pixmap(pmap)
|
self.set_pixmap(pmap)
|
||||||
self.cover_changed.emit(open(path, 'rb').read())
|
self.cover_changed.emit(pixmap_to_data(pmap))
|
||||||
|
|
||||||
def dragMoveEvent(self, event):
|
def dragMoveEvent(self, event):
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
|
@ -12,13 +12,13 @@ from calibre.constants import DEBUG
|
|||||||
from calibre.utils.config import Config, StringConfig, tweaks
|
from calibre.utils.config import Config, StringConfig, tweaks
|
||||||
from calibre.utils.formatter import TemplateFormatter
|
from calibre.utils.formatter import TemplateFormatter
|
||||||
from calibre.utils.filenames import shorten_components_to, supports_long_names, \
|
from calibre.utils.filenames import shorten_components_to, supports_long_names, \
|
||||||
ascii_filename, sanitize_file_name
|
ascii_filename
|
||||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||||
from calibre.ebooks.metadata.meta import set_metadata
|
from calibre.ebooks.metadata.meta import set_metadata
|
||||||
from calibre.constants import preferred_encoding, filesystem_encoding
|
from calibre.constants import preferred_encoding
|
||||||
from calibre.ebooks.metadata import fmt_sidx
|
from calibre.ebooks.metadata import fmt_sidx
|
||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort
|
||||||
from calibre import strftime, prints
|
from calibre import strftime, prints, sanitize_file_name_unicode
|
||||||
|
|
||||||
plugboard_any_device_value = 'any device'
|
plugboard_any_device_value = 'any device'
|
||||||
plugboard_any_format_value = 'any format'
|
plugboard_any_format_value = 'any format'
|
||||||
@ -197,12 +197,10 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
|||||||
format_args[key] = ''
|
format_args[key] = ''
|
||||||
components = SafeFormat().safe_format(template, format_args,
|
components = SafeFormat().safe_format(template, format_args,
|
||||||
'G_C-EXCEPTION!', mi)
|
'G_C-EXCEPTION!', mi)
|
||||||
components = [x.strip() for x in components.split('/') if x.strip()]
|
components = [x.strip() for x in components.split('/')]
|
||||||
components = [sanitize_func(x) for x in components if x]
|
components = [sanitize_func(x) for x in components if x]
|
||||||
if not components:
|
if not components:
|
||||||
components = [str(id)]
|
components = [str(id)]
|
||||||
components = [x.encode(filesystem_encoding, 'replace') if isinstance(x,
|
|
||||||
unicode) else x for x in components]
|
|
||||||
if to_lowercase:
|
if to_lowercase:
|
||||||
components = [x.lower() for x in components]
|
components = [x.lower() for x in components]
|
||||||
if replace_whitespace:
|
if replace_whitespace:
|
||||||
@ -247,7 +245,7 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
|
|||||||
return True, id_, mi.title
|
return True, id_, mi.title
|
||||||
|
|
||||||
components = get_components(opts.template, mi, id_, opts.timefmt, length,
|
components = get_components(opts.template, mi, id_, opts.timefmt, length,
|
||||||
ascii_filename if opts.asciiize else sanitize_file_name,
|
ascii_filename if opts.asciiize else sanitize_file_name_unicode,
|
||||||
to_lowercase=opts.to_lowercase,
|
to_lowercase=opts.to_lowercase,
|
||||||
replace_whitespace=opts.replace_whitespace)
|
replace_whitespace=opts.replace_whitespace)
|
||||||
base_path = os.path.join(root, *components)
|
base_path = os.path.join(root, *components)
|
||||||
@ -329,8 +327,6 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
|
|||||||
def _sanitize_args(root, opts):
|
def _sanitize_args(root, opts):
|
||||||
if opts is None:
|
if opts is None:
|
||||||
opts = config().parse()
|
opts = config().parse()
|
||||||
if isinstance(root, unicode):
|
|
||||||
root = root.encode(filesystem_encoding)
|
|
||||||
root = os.path.abspath(root)
|
root = os.path.abspath(root)
|
||||||
|
|
||||||
opts.template = preprocess_template(opts.template)
|
opts.template = preprocess_template(opts.template)
|
||||||
|
@ -72,47 +72,6 @@ if not _run_once:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Improve builtin path functions to handle unicode sensibly
|
|
||||||
|
|
||||||
_abspath = os.path.abspath
|
|
||||||
def my_abspath(path, encoding=sys.getfilesystemencoding()):
|
|
||||||
'''
|
|
||||||
Work around for buggy os.path.abspath. This function accepts either byte strings,
|
|
||||||
in which it calls os.path.abspath, or unicode string, in which case it first converts
|
|
||||||
to byte strings using `encoding`, calls abspath and then decodes back to unicode.
|
|
||||||
'''
|
|
||||||
to_unicode = False
|
|
||||||
if encoding is None:
|
|
||||||
encoding = preferred_encoding
|
|
||||||
if isinstance(path, unicode):
|
|
||||||
path = path.encode(encoding)
|
|
||||||
to_unicode = True
|
|
||||||
res = _abspath(path)
|
|
||||||
if to_unicode:
|
|
||||||
res = res.decode(encoding)
|
|
||||||
return res
|
|
||||||
|
|
||||||
os.path.abspath = my_abspath
|
|
||||||
|
|
||||||
_join = os.path.join
|
|
||||||
def my_join(a, *p):
|
|
||||||
encoding=sys.getfilesystemencoding()
|
|
||||||
if not encoding:
|
|
||||||
encoding = preferred_encoding
|
|
||||||
p = [a] + list(p)
|
|
||||||
_unicode = False
|
|
||||||
for i in p:
|
|
||||||
if isinstance(i, unicode):
|
|
||||||
_unicode = True
|
|
||||||
break
|
|
||||||
p = [i.encode(encoding) if isinstance(i, unicode) else i for i in p]
|
|
||||||
|
|
||||||
res = _join(*p)
|
|
||||||
if _unicode:
|
|
||||||
res = res.decode(encoding)
|
|
||||||
return res
|
|
||||||
|
|
||||||
os.path.join = my_join
|
|
||||||
|
|
||||||
def local_open(name, mode='r', bufsize=-1):
|
def local_open(name, mode='r', bufsize=-1):
|
||||||
'''
|
'''
|
||||||
|
@ -19,7 +19,7 @@ in the working tree you want to use it with::
|
|||||||
trac_reponame_password = <password>
|
trac_reponame_password = <password>
|
||||||
|
|
||||||
'''
|
'''
|
||||||
import os, re, xmlrpclib
|
import os, re, xmlrpclib, subprocess
|
||||||
from bzrlib.builtins import cmd_commit as _cmd_commit, tree_files
|
from bzrlib.builtins import cmd_commit as _cmd_commit, tree_files
|
||||||
from bzrlib import branch
|
from bzrlib import branch
|
||||||
import bzrlib
|
import bzrlib
|
||||||
@ -115,5 +115,7 @@ class cmd_commit(_cmd_commit):
|
|||||||
server.ticket.update(int(bug), msg,
|
server.ticket.update(int(bug), msg,
|
||||||
{'status':'closed', 'resolution':'fixed'},
|
{'status':'closed', 'resolution':'fixed'},
|
||||||
True)
|
True)
|
||||||
|
subprocess.Popen('/home/kovid/work/kde/mail.py -f --delay 10'.split())
|
||||||
|
|
||||||
|
|
||||||
bzrlib.commands.register_command(cmd_commit)
|
bzrlib.commands.register_command(cmd_commit)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user