Implement a 'Browse books by cover' feature

This commit is contained in:
Kovid Goyal 2008-05-23 19:07:41 -07:00
parent d669477b23
commit fcf35ac0b3
11 changed files with 372 additions and 97 deletions

View File

@ -25,15 +25,17 @@ manual:
pictureflow : pictureflow :
mkdir -p src/calibre/plugins && rm -f src/calibre/plugins/*pictureflow* && \ mkdir -p src/calibre/plugins && rm -f src/calibre/plugins/*pictureflow* && \
cd src/calibre/gui2/pictureflow && rm -f *.o *.so* && \ cd src/calibre/gui2/pictureflow && rm -f *.o && \
mkdir -p .build && cd .build && rm -f * && \ mkdir -p .build && cd .build && rm -f * && \
qmake ../pictureflow-lib.pro && make && \ qmake ../pictureflow.pro && make && \
cd ../PyQt && \ cd ../PyQt && \
mkdir -p .build && \ mkdir -p .build && \
cd .build && rm -f * && \ cd .build && rm -f * && \
python ../configure.py && make && \ python ../configure.py && make && \
cd ../../../../../.. && \ cd ../../../../../.. && \
cp src/calibre/gui2/pictureflow/libpictureflow.so.?.?.? src/calibre/gui2/pictureflow/PyQt/.build/pictureflow.so src/calibre/plugins/ && \ cp src/calibre/gui2/pictureflow/.build/libpictureflow.so.?.?.? src/calibre/gui2/pictureflow/PyQt/.build/pictureflow.so src/calibre/plugins/ && \
python -c "import os, glob; lp = glob.glob('src/calibre/plugins/libpictureflow.so.*')[0]; os.rename(lp, lp[:-4])" python -c "import os, glob; lp = glob.glob('src/calibre/plugins/libpictureflow.so.*')[0]; os.rename(lp, lp[:-4])" && \
rm -rf src/calibre/gui2/pictureflow/.build rm -rf src/calibre/gui2/pictureflow/PyQt/.build

View File

@ -177,11 +177,18 @@ _check_symlinks_prescript()
try: try:
print 'Building pictureflow' print 'Building pictureflow'
os.chdir('src/calibre/gui2/pictureflow') os.chdir('src/calibre/gui2/pictureflow')
for f in glob.glob('*.o'): os.unlink(f) if not os.path.exists('.build'):
subprocess.check_call([qmake, 'pictureflow.pro']) os.mkdir('.build')
os.chdir('.build')
for f in glob.glob('*'): os.unlink(f)
subprocess.check_call([qmake, '../pictureflow.pro'])
subprocess.check_call(['make']) subprocess.check_call(['make'])
files.append((os.path.abspath(os.path.realpath('libpictureflow.dylib')), 'libpictureflow.dylib')) files.append((os.path.abspath(os.path.realpath('libpictureflow.dylib')), 'libpictureflow.dylib'))
os.chdir('PyQt/.build') os.chdir('../PyQt')
if not os.path.exists('.build'):
os.mkdir('.build')
os.chdir('.build')
for f in glob.glob('*'): os.unlink(f)
subprocess.check_call([PYTHON, '../configure.py']) subprocess.check_call([PYTHON, '../configure.py'])
subprocess.check_call(['/usr/bin/make']) subprocess.check_call(['/usr/bin/make'])
files.append((os.path.abspath('pictureflow.so'), 'pictureflow.so')) files.append((os.path.abspath('pictureflow.so'), 'pictureflow.so'))

View File

@ -555,6 +555,9 @@ if islinux:
sys.path.insert(1, plugins) sys.path.insert(1, plugins)
cwd = os.getcwd() cwd = os.getcwd()
os.chdir(plugins) os.chdir(plugins)
if iswindows and hasattr(sys, 'frozen'):
sys.path.insert(1, os.path.dirname(sys.executable))
try: try:
import pictureflow import pictureflow
pictureflowerror = '' pictureflowerror = ''

View File

@ -8,14 +8,17 @@ Module to implement the Cover Flow feature
''' '''
import sys, os import sys, os
from collections import deque
from PyQt4.QtGui import QImage from PyQt4.QtGui import QImage, QSizePolicy
from PyQt4.QtCore import Qt, QSize, QTimer, SIGNAL from PyQt4.QtCore import Qt, QSize, SIGNAL
from calibre import pictureflow from calibre import pictureflow
if pictureflow is not None: if pictureflow is not None:
class EmptyImageList(pictureflow.FlowImages):
def __init__(self):
pictureflow.FlowImages.__init__(self)
class FileSystemImages(pictureflow.FlowImages): class FileSystemImages(pictureflow.FlowImages):
def __init__(self, dirpath): def __init__(self, dirpath):
@ -46,60 +49,35 @@ if pictureflow is not None:
def __init__(self, model, buffer=20): def __init__(self, model, buffer=20):
pictureflow.FlowImages.__init__(self) pictureflow.FlowImages.__init__(self)
self.model = model self.model = model
self.default_image = QImage(':/images/book.svg') self.connect(self.model, SIGNAL('modelReset()'), self.reset)
self.buffer_size = buffer
self.timer = QTimer()
self.connect(self.timer, SIGNAL('timeout()'), self.load)
self.timer.start(50)
self.clear()
def count(self): def count(self):
return self.model.rowCount(None) return self.model.count()
def caption(self, index): def caption(self, index):
return self.model.title(index) return self.model.title(index)
def clear(self): def reset(self):
self.buffer = {} self.emit(SIGNAL('dataChanged()'))
self.load_queue = deque()
def load(self):
if self.load_queue:
index = self.load_queue.popleft()
if self.buffer.has_key(index):
return
img = QImage()
img.loadFromData(self.model.cover(index))
if img.isNull():
img = self.default_image
self.buffer[index] = img
def image(self, index): def image(self, index):
img = self.buffer.get(index) return self.model.cover(index)
if img is None:
img = QImage()
img.loadFromData(self.model.cover(index))
if img.isNull():
img = self.default_image
self.buffer[index] = img
return img
def currentChanged(self, index):
for key in self.buffer.keys():
if abs(key - index) > self.buffer_size:
self.buffer.pop(key)
for i in range(max(0, index-self.buffer_size), min(self.count(), index+self.buffer_size)):
if not self.buffer.has_key(i):
self.load_queue.append(i)
class CoverFlow(pictureflow.PictureFlow): class CoverFlow(pictureflow.PictureFlow):
def __init__(self, height=300, parent=None): def __init__(self, height=300, parent=None):
pictureflow.PictureFlow.__init__(self, parent) pictureflow.PictureFlow.__init__(self, parent)
self.setSlideSize(QSize(int(2/3. * height), height)) self.setSlideSize(QSize(int(2/3. * height), height))
self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height)) self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+25))
self.setFocusPolicy(Qt.WheelFocus)
self.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum))
else: else:
CoverFlow = None CoverFlow = None
DatabaseImages = None
FileSystemImages = None
def main(args=sys.argv): def main(args=sys.argv):
return 0 return 0
@ -112,15 +90,6 @@ if __name__ == '__main__':
cf.resize(cf.minimumSize()) cf.resize(cf.minimumSize())
w.resize(cf.minimumSize()+QSize(30, 20)) w.resize(cf.minimumSize()+QSize(30, 20))
path = sys.argv[1] path = sys.argv[1]
if path.endswith('.db'):
from calibre.library.database import LibraryDatabase
from calibre.gui2.library import BooksModel
from calibre.gui2 import images_rc
bm = BooksModel()
bm.set_database(LibraryDatabase(path))
bm.sort(1, Qt.AscendingOrder)
model = DatabaseImages(bm)
else:
model = FileSystemImages(sys.argv[1]) model = FileSystemImages(sys.argv[1])
cf.setImages(model) cf.setImages(model)
cf.connect(cf, SIGNAL('currentChanged(int)'), model.currentChanged) cf.connect(cf, SIGNAL('currentChanged(int)'), model.currentChanged)

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
''' '''
import textwrap import textwrap
from PyQt4.QtCore import Qt, QCoreApplication from PyQt4.QtCore import QCoreApplication
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
@ -19,11 +19,6 @@ class BookInfo(QDialog, Ui_BookInfo):
Ui_BookInfo.__init__(self) Ui_BookInfo.__init__(self)
self.setupUi(self) self.setupUi(self)
self.default_pixmap = QPixmap(':/images/book.svg').scaled(80,
100,
Qt.IgnoreAspectRatio,
Qt.SmoothTransformation)
self.setWindowTitle(info[_('Title')]) self.setWindowTitle(info[_('Title')])
desktop = QCoreApplication.instance().desktop() desktop = QCoreApplication.instance().desktop()
screen_height = desktop.availableGeometry().height() - 100 screen_height = desktop.availableGeometry().height() - 100
@ -32,11 +27,7 @@ class BookInfo(QDialog, Ui_BookInfo):
self.comments.setText(info.pop(_('Comments'), '')) self.comments.setText(info.pop(_('Comments'), ''))
cdata = info.pop('cover', '') cdata = info.pop('cover', '')
pixmap = QPixmap() pixmap = QPixmap.fromImage(cdata)
pixmap.loadFromData(cdata)
if pixmap.isNull():
pixmap = self.default_pixmap
self.setWindowIcon(QIcon(pixmap)) self.setWindowIcon(QIcon(pixmap))
self.scene = QGraphicsScene() self.scene = QGraphicsScene()

View File

@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.45"
version="1.0"
sodipodi:docname="emblem-link.svgz"
sodipodi:docbase="/home/david/Oxygen/Oxygen/scalable/emblems"
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
inkscape:export-filename="/home/david/Oxygen/Oxygen/scalable/emblems/emblem-link.png"
inkscape:export-xdpi="33.75"
inkscape:export-ydpi="33.75"
sodipodi:modified="TRUE">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient3185">
<stop
style="stop-color:#eeeeee;stop-opacity:1;"
offset="0"
id="stop3187" />
<stop
style="stop-color:#eeeeee;stop-opacity:0;"
offset="1"
id="stop3189" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3133">
<stop
style="stop-color:#646661;stop-opacity:1"
offset="0"
id="stop3135" />
<stop
style="stop-color:#111111;stop-opacity:1"
offset="1"
id="stop3137" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3133"
id="radialGradient3139"
cx="64"
cy="35.686314"
fx="64"
fy="35.686314"
r="40"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.1360441,0,0,1.446027,-72.706823,-26.184217)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3185"
id="linearGradient3191"
x1="112"
y1="98.41069"
x2="61.978939"
y2="11.771669"
gradientUnits="userSpaceOnUse" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath3393">
<path
style="opacity:1;fill:#dbdbdb;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 13.8125,8 C 10.584167,8 8,10.584167 8,13.8125 L 8,114.1875 C 8,117.41583 10.584167,120 13.8125,120 L 114.1875,120 C 117.41583,120 120,117.41583 120,114.1875 L 120,13.8125 C 120,10.584167 117.41583,8 114.1875,8 L 13.8125,8 z M 21.8125,16 L 106.1875,16 C 109.41583,16 112,18.584167 112,21.8125 L 112,106.1875 C 112,109.41583 109.41583,112 106.1875,112 L 21.8125,112 C 18.584166,112 16,109.41583 16,106.1875 L 16,21.8125 C 16,18.584166 18.584167,16 21.8125,16 z "
id="path3395" />
</clipPath>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3133"
id="radialGradient3413"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.1360441,0,0,-1.446027,-72.706823,154.18422)"
cx="64"
cy="35.686314"
fx="64"
fy="35.686314"
r="40" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3133"
id="radialGradient3417"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.1360441,0,0,1.446027,-72.706823,-25.184217)"
cx="64"
cy="35.686314"
fx="64"
fy="35.686314"
r="40" />
<filter
inkscape:collect="always"
id="filter3351">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="2"
id="feGaussianBlur3353" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
gridtolerance="10000"
guidetolerance="10"
objecttolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="4.9765625"
inkscape:cx="64"
inkscape:cy="64"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="128px"
height="128px"
gridempspacing="2"
gridspacingx="4px"
gridspacingy="4px"
showgrid="false"
inkscape:window-width="748"
inkscape:window-height="734"
inkscape:window-x="400"
inkscape:window-y="0"
inkscape:grid-points="true" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="opacity:0.7;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter3351)"
id="rect3305"
width="100"
height="100"
x="14"
y="14"
rx="7.4348507"
ry="7.4348507"
clip-path="url(#clipPath3393)" />
<rect
ry="5.6263733"
rx="5.6263733"
y="16"
x="16"
height="96"
width="96"
id="rect3141"
style="opacity:0.7;fill:#eeeeee;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:1;fill:url(#radialGradient3139);fill-opacity:1.0;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect2160"
width="80"
height="80"
x="24"
y="24"
rx="5.6263733"
ry="5.6263733" />
<rect
style="opacity:0.7;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect3178"
width="95"
height="95"
x="16.5"
y="16.5"
rx="5.6263733"
ry="5.6263733" />
<g
id="g3160"
transform="matrix(1.6656201,0,0,1.6656201,-62.574569,-26.624804)"
style="fill:#ffffff;fill-opacity:1">
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 44.03125 40 L 55.34375 51.3125 C 43.467571 59.035057 35.734919 71.703669 57.34375 93.3125 C 52.734046 74.873684 60.878036 66.021115 70.75 66.71875 L 84 79.96875 L 84 73.3125 L 84 40 L 44.03125 40 z "
transform="matrix(0.600377,0,0,0.600377,37.568332,15.98492)"
id="path2179" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -3,13 +3,15 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, textwrap, traceback, time, re, sre_constants import os, textwrap, traceback, time, re, sre_constants
from datetime import timedelta, datetime from datetime import timedelta, datetime
from operator import attrgetter from operator import attrgetter
from collections import deque
from math import cos, sin, pi from math import cos, sin, pi
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \ from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QLineEdit, QApplication, \ QPen, QStyle, QPainter, QLineEdit, QApplication, \
QPalette QPalette, QImage
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex QCoreApplication, SIGNAL, QObject, QSize, QModelIndex, \
QTimer
from calibre import Settings, preferred_encoding from calibre import Settings, preferred_encoding
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
@ -92,14 +94,24 @@ class BooksModel(QAbstractTableModel):
num -= d num -= d
return ''.join(result) return ''.join(result)
def __init__(self, parent=None): def __init__(self, parent=None, buffer=20):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
self.db = None self.db = None
self.cols = ['title', 'authors', 'size', 'date', 'rating', 'publisher', 'tags', 'series'] self.cols = ['title', 'authors', 'size', 'date', 'rating', 'publisher', 'tags', 'series']
self.editable_cols = [0, 1, 4, 5, 6] self.editable_cols = [0, 1, 4, 5, 6]
self.default_image = QImage(':/images/book.svg')
self.sorted_on = (3, Qt.AscendingOrder) self.sorted_on = (3, Qt.AscendingOrder)
self.last_search = '' # The last search performed on this model self.last_search = '' # The last search performed on this model
self.read_config() self.read_config()
self.buffer_size = buffer
self.clear_caches()
self.load_timer = QTimer()
self.connect(self.load_timer, SIGNAL('timeout()'), self.load)
self.load_timer.start(50)
def clear_caches(self):
self.buffer = {}
self.load_queue = deque()
def read_config(self): def read_config(self):
self.use_roman_numbers = bool(Settings().value('use roman numerals for series number', self.use_roman_numbers = bool(Settings().value('use roman numerals for series number',
@ -164,6 +176,7 @@ class BooksModel(QAbstractTableModel):
self.db.filter(tokens, refilter=refinement, OR=OR) self.db.filter(tokens, refilter=refinement, OR=OR)
self.last_search = text self.last_search = text
if reset: if reset:
self.clear_caches()
self.reset() self.reset()
def sort(self, col, order, reset=True): def sort(self, col, order, reset=True):
@ -173,6 +186,7 @@ class BooksModel(QAbstractTableModel):
self.db.refresh(self.cols[col], ascending) self.db.refresh(self.cols[col], ascending)
self.research() self.research()
if reset: if reset:
self.clear_caches()
self.reset() self.reset()
self.sorted_on = (col, order) self.sorted_on = (col, order)
@ -194,10 +208,32 @@ class BooksModel(QAbstractTableModel):
def rowCount(self, parent): def rowCount(self, parent):
return self.db.rows() if self.db else 0 return self.db.rows() if self.db else 0
def count(self):
return self.rowCount(None)
def load(self):
if self.load_queue:
index = self.load_queue.popleft()
if self.buffer.has_key(index):
return
data = self.db.cover(index)
img = QImage()
img.loadFromData(data)
if img.isNull():
img = self.default_image
self.buffer[index] = img
def current_changed(self, current, previous, emit_signal=True): def current_changed(self, current, previous, emit_signal=True):
data = {} data = {}
idx = current.row() idx = current.row()
cdata = self.db.cover(idx) cdata = self.cover(idx)
for key in self.buffer.keys():
if abs(key - idx) > self.buffer_size:
self.buffer.pop(key)
for i in range(max(0, idx-self.buffer_size), min(self.count(), idx+self.buffer_size)):
if not self.buffer.has_key(i):
self.load_queue.append(i)
if cdata: if cdata:
data['cover'] = cdata data['cover'] = cdata
tags = self.db.tags(idx) tags = self.db.tags(idx)
@ -296,7 +332,15 @@ class BooksModel(QAbstractTableModel):
return self.db.title(row_number) return self.db.title(row_number)
def cover(self, row_number): def cover(self, row_number):
return self.db.cover(row_number) img = self.buffer.get(row_number, -1)
if img == -1:
data = self.db.cover(row_number)
img = QImage()
img.loadFromData(data)
if img.isNull():
img = self.default_image
self.buffer[row_number] = img
return img
def data(self, index, role): def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole: if role == Qt.DisplayRole or role == Qt.EditRole:

View File

@ -5,7 +5,7 @@ from xml.parsers.expat import ExpatError
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
QVariant, QThread, QString QVariant, QThread, QString
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \ from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \
QToolButton, QDialog QToolButton, QDialog, QSizePolicy
from PyQt4.QtSvg import QSvgRenderer from PyQt4.QtSvg import QSvgRenderer
from calibre import __version__, __appname__, islinux, sanitize_file_name, launch, \ from calibre import __version__, __appname__, islinux, sanitize_file_name, launch, \
@ -19,7 +19,7 @@ from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \
pixmap_to_data, choose_dir, ORG_NAME, \ pixmap_to_data, choose_dir, ORG_NAME, \
qstring_to_unicode, set_sidebar_directories, \ qstring_to_unicode, set_sidebar_directories, \
SingleApplication, Application SingleApplication, Application
from calibre.gui2.cover_flow import CoverFlow from calibre.gui2.cover_flow import CoverFlow, DatabaseImages
from calibre.library.database import LibraryDatabase from calibre.library.database import LibraryDatabase
from calibre.gui2.update import CheckForUpdates from calibre.gui2.update import CheckForUpdates
from calibre.gui2.main_window import MainWindow from calibre.gui2.main_window import MainWindow
@ -199,6 +199,25 @@ class Main(MainWindow, Ui_MainWindow):
self.library_view.resizeRowsToContents() self.library_view.resizeRowsToContents()
self.search.setFocus(Qt.OtherFocusReason) self.search.setFocus(Qt.OtherFocusReason)
########################### Cover Flow ################################
self.cover_flow = None
if CoverFlow is not None:
self.cover_flow = CoverFlow(height=220)
self.cover_flow.setVisible(False)
self.library.layout().addWidget(self.cover_flow)
self.connect(self.cover_flow, SIGNAL('currentChanged(int)'), self.sync_cf_to_listview)
self.library_view.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Expanding))
self.connect(self.cover_flow, SIGNAL('itemActivated(int)'), self.show_book_info)
self.connect(self.status_bar.cover_flow_button, SIGNAL('toggled(bool)'), self.toggle_cover_flow)
QObject.connect(self.library_view.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self.sync_cf_to_listview)
self.db_images = DatabaseImages(self.library_view.model())
self.cover_flow.setImages(self.db_images)
else:
self.status_bar.cover_flow_button.disable(pictureflowerror)
####################### Setup device detection ######################## ####################### Setup device detection ########################
self.detector = DeviceDetector(sleep_time=2000) self.detector = DeviceDetector(sleep_time=2000)
QObject.connect(self.detector, SIGNAL('connected(PyQt_PyObject, PyQt_PyObject)'), QObject.connect(self.detector, SIGNAL('connected(PyQt_PyObject, PyQt_PyObject)'),
@ -207,6 +226,29 @@ class Main(MainWindow, Ui_MainWindow):
self.news_menu.set_custom_feeds(self.library_view.model().db.get_feeds()) self.news_menu.set_custom_feeds(self.library_view.model().db.get_feeds())
def toggle_cover_flow(self, show):
if show:
self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row())
self.cover_flow.setVisible(True)
self.cover_flow.setFocus(Qt.OtherFocusReason)
self.status_bar.book_info.book_data.setMaximumHeight(100)
self.status_bar.setMaximumHeight(120)
self.library_view.scrollTo(self.library_view.currentIndex())
else:
self.cover_flow.setVisible(False)
self.status_bar.book_info.book_data.setMaximumHeight(1000)
self.status_bar.setMaximumHeight(1200)
def sync_cf_to_listview(self, index, *args):
if not hasattr(index, 'row') and self.library_view.currentIndex().row() != index:
index = self.library_view.model().index(index, 0)
self.library_view.setCurrentIndex(index)
if hasattr(index, 'row') and self.cover_flow.isVisible() and self.cover_flow.currentSlide() != index.row():
self.cover_flow.setCurrentSlide(index.row())
def another_instance_wants_to_talk(self, msg): def another_instance_wants_to_talk(self, msg):
if msg.startswith('launched:'): if msg.startswith('launched:'):
argv = eval(msg[len('launched:'):]) argv = eval(msg[len('launched:'):])
@ -942,7 +984,7 @@ class Main(MainWindow, Ui_MainWindow):
################################ Book info ################################# ################################ Book info #################################
def show_book_info(self): def show_book_info(self, *args):
if self.current_view() is not self.library_view: if self.current_view() is not self.library_view:
error_dialog(self, _('No detailed info available'), error_dialog(self, _('No detailed info available'),
_('No detailed information is available for books on the device.')).exec_() _('No detailed information is available for books on the device.')).exec_()

View File

@ -30,7 +30,7 @@ makefile = pyqtconfig.QtGuiModuleMakefile (
# Add the library we are wrapping. The name doesn't include any platform # Add the library we are wrapping. The name doesn't include any platform
# specific prefixes or extensions (e.g. the "lib" prefix on UNIX, or the # specific prefixes or extensions (e.g. the "lib" prefix on UNIX, or the
# ".dll" extension on Windows). # ".dll" extension on Windows).
makefile.extra_lib_dirs = ['../..', '..\\..\\release'] makefile.extra_lib_dirs = ['..\\..\\.build\\release', '../../.build']
makefile.extra_libs = ['pictureflow0' if 'win' in sys.platform and 'darwin' not in sys.platform else "pictureflow"] makefile.extra_libs = ['pictureflow0' if 'win' in sys.platform and 'darwin' not in sys.platform else "pictureflow"]
makefile.extra_cflags = ['-arch i386', '-arch ppc'] if 'darwin' in sys.platform else [] makefile.extra_cflags = ['-arch i386', '-arch ppc'] if 'darwin' in sys.platform else []
makefile.extra_cxxflags = makefile.extra_cflags makefile.extra_cxxflags = makefile.extra_cflags

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import textwrap import textwrap
from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \ from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \
QVBoxLayout, QSizePolicy QVBoxLayout, QSizePolicy, QToolButton, QIcon
from PyQt4.QtCore import Qt, QSize, SIGNAL from PyQt4.QtCore import Qt, QSize, SIGNAL
from calibre import fit_image from calibre import fit_image
from calibre.gui2 import qstring_to_unicode from calibre.gui2 import qstring_to_unicode
@ -64,13 +64,7 @@ class BookInfoDisplay(QFrame):
def show_data(self, data): def show_data(self, data):
if data.has_key('cover'): if data.has_key('cover'):
cover_data = data.pop('cover') self.cover_display.setPixmap(QPixmap.fromImage(data.pop('cover')))
pixmap = QPixmap()
pixmap.loadFromData(cover_data)
if pixmap.isNull():
self.cover_display.setPixmap(self.cover_display.default_pixmap)
else:
self.cover_display.setPixmap(pixmap)
else: else:
self.cover_display.setPixmap(self.cover_display.default_pixmap) self.cover_display.setPixmap(self.cover_display.default_pixmap)
@ -121,15 +115,38 @@ class MovieButton(QFrame):
self.jobs_dialog.show() self.jobs_dialog.show()
self.jobs_dialog.jobs_view.restore_column_widths() self.jobs_dialog.jobs_view.restore_column_widths()
class CoverFlowButton(QToolButton):
def __init__(self, parent=None):
QToolButton.__init__(self, parent)
self.setIconSize(QSize(80, 80))
self.setIcon(QIcon(':/images/cover_flow.svg'))
self.setCheckable(True)
self.setChecked(False)
self.setAutoRaise(True)
self.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding))
self.connect(self, SIGNAL('toggled(bool)'), self.adjust_tooltip)
self.adjust_tooltip(False)
def adjust_tooltip(self, on):
tt = _('Click to turn off Cover Browsing') if on else _('Click to browse books by their covers')
self.setToolTip(tt)
def disable(self, reason):
self.setDisabled(True)
self.setToolTip(_('<p>Browsing books by their covers is disabled.<br>Import of pictureflow module failed:<br>')+reason)
class StatusBar(QStatusBar): class StatusBar(QStatusBar):
def __init__(self, jobs_dialog): def __init__(self, jobs_dialog):
QStatusBar.__init__(self) QStatusBar.__init__(self)
self.movie_button = MovieButton(QMovie(':/images/jobs-animated.mng'), jobs_dialog) self.movie_button = MovieButton(QMovie(':/images/jobs-animated.mng'), jobs_dialog)
self.cover_flow_button = CoverFlowButton()
self.addPermanentWidget(self.cover_flow_button)
self.addPermanentWidget(self.movie_button) self.addPermanentWidget(self.movie_button)
self.book_info = BookInfoDisplay(self.clearMessage) self.book_info = BookInfoDisplay(self.clearMessage)
self.connect(self.book_info, SIGNAL('show_book_info()'), self.show_book_info) self.connect(self.book_info, SIGNAL('show_book_info()'), self.show_book_info)
self.addWidget(self.book_info) self.addWidget(self.book_info)
self.setMinimumHeight(120)
def reset_info(self): def reset_info(self):
self.book_info.show_data({}) self.book_info.show_data({})

View File

@ -539,26 +539,27 @@ class BuildEXE(build_exe):
''' '''
def build_plugins(self): def build_plugins(self):
cwd = os.getcwd() cwd = os.getcwd()
dd = os.path.join(cwd, self.dist_dir)
try: try:
os.chdir(os.path.join('src', 'calibre', 'gui2', 'pictureflow')) os.chdir(os.path.join('src', 'calibre', 'gui2', 'pictureflow'))
if os.path.exists('release'): if os.path.exists('.build'):
shutil.rmtree('release') shutil.rmtree('.build')
if os.path.exists('debug'): os.mkdir('.build')
shutil.rmtree('debug') os.chdir('.build')
subprocess.check_call(['qmake', 'pictureflow.pro']) subprocess.check_call(['qmake', '../pictureflow.pro'])
subprocess.check_call(['mingw32-make', '-f', 'Makefile.Release']) subprocess.check_call(['mingw32-make', '-f', 'Makefile.Release'])
os.chdir('PyQt') shutil.copyfile('release\\pictureflow0.dll', os.path.join(dd, 'pictureflow0.dll'))
os.chdir('..\\PyQt')
if not os.path.exists('.build'): if not os.path.exists('.build'):
os.mkdir('.build') os.mkdir('.build')
os.chdir('.build') os.chdir('.build')
subprocess.check_call(['python', '..\\configure.py']) subprocess.check_call(['python', '..\\configure.py'])
subprocess.check_call(['mingw32-make', '-f', 'Makefile']) subprocess.check_call(['mingw32-make', '-f', 'Makefile'])
dd = os.path.join(cwd, self.dist_dir)
shutil.copyfile('pictureflow.pyd', os.path.join(dd, 'pictureflow.pyd')) shutil.copyfile('pictureflow.pyd', os.path.join(dd, 'pictureflow.pyd'))
os.chdir('..\\..') os.chdir('..')
shutil.copyfile('release\\pictureflow0.dll', os.path.join(dd, 'pictureflow0.dll')) shutil.rmtree('.build')
shutil.rmtree('Release', True) os.chdir('..')
shutil.rmtree('Debug', True) shutil.rmtree('.build')
finally: finally:
os.chdir(cwd) os.chdir(cwd)