diff --git a/resources/images/textures/dark_cloth.png b/resources/images/textures/dark_cloth.png new file mode 100644 index 0000000000..17a4d6b42d Binary files /dev/null and b/resources/images/textures/dark_cloth.png differ diff --git a/resources/images/textures/dark_wood.png b/resources/images/textures/dark_wood.png new file mode 100644 index 0000000000..6e01b3d6f8 Binary files /dev/null and b/resources/images/textures/dark_wood.png differ diff --git a/resources/images/textures/grey_wash_wall.png b/resources/images/textures/grey_wash_wall.png new file mode 100644 index 0000000000..d54e889949 Binary files /dev/null and b/resources/images/textures/grey_wash_wall.png differ diff --git a/resources/images/textures/light_wood.png b/resources/images/textures/light_wood.png new file mode 100644 index 0000000000..0b4c35f8c3 Binary files /dev/null and b/resources/images/textures/light_wood.png differ diff --git a/resources/images/textures/subtle_wood.png b/resources/images/textures/subtle_wood.png new file mode 100644 index 0000000000..22f2450d3a Binary files /dev/null and b/resources/images/textures/subtle_wood.png differ diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 816ed5b617..67d4b80750 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -118,6 +118,7 @@ defs['cover_grid_color'] = (80, 80, 80) defs['cover_grid_cache_size'] = 100 defs['cover_grid_disk_cache_size'] = 2500 defs['cover_grid_show_title'] = False +defs['cover_grid_texture'] = None defs['show_vl_tabs'] = False del defs # }}} diff --git a/src/calibre/gui2/library/alternate_views.py b/src/calibre/gui2/library/alternate_views.py index a3885bf135..75178b09a7 100644 --- a/src/calibre/gui2/library/alternate_views.py +++ b/src/calibre/gui2/library/alternate_views.py @@ -19,7 +19,7 @@ from PyQt4.Qt import ( QTimer, QPalette, QColor, QItemSelection, QPixmap, QMenu, QApplication, QMimeData, QUrl, QDrag, QPoint, QPainter, QRect, pyqtProperty, QEvent, QPropertyAnimation, QEasingCurve, pyqtSlot, QHelpEvent, QAbstractItemView, - QStyleOptionViewItem, QToolTip, QByteArray, QBuffer) + QStyleOptionViewItem, QToolTip, QByteArray, QBuffer, QBrush) from calibre import fit_image, prints, prepare_string_for_xml from calibre.ebooks.metadata import fmt_sidx @@ -586,6 +586,12 @@ class GridView(QListView): pal = QPalette() col = QColor(r, g, b) pal.setColor(pal.Base, col) + tex = gprefs['cover_grid_texture'] + if tex: + from calibre.gui2.preferences.texture_chooser import texture_path + path = texture_path(tex) + if path: + pal.setBrush(pal.Base, QBrush(QPixmap(path))) dark = (r + g + b)/3.0 < 128 pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black)) self.setPalette(pal) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 4fc32b0e77..9e26db4563 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -8,13 +8,15 @@ __docformat__ = 'restructuredtext en' from threading import Thread from functools import partial -from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, QColorDialog, - QAbstractListModel, Qt, QIcon, QKeySequence, QPalette, QColor, pyqtSignal) +from PyQt4.Qt import ( + QApplication, QFont, QFontInfo, QFontDialog, QColorDialog, QPainter, + QAbstractListModel, Qt, QIcon, QKeySequence, QColor, pyqtSignal, + QWidget, QSizePolicy, QBrush, QPixmap, QSize, QPushButton) from calibre import human_readable from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences.look_feel_ui import Ui_Form -from calibre.gui2 import config, gprefs, qt_app, NONE, open_local_file +from calibre.gui2 import config, gprefs, qt_app, NONE, open_local_file, question_dialog from calibre.utils.localization import (available_translations, get_language, get_lang) from calibre.utils.config import prefs @@ -98,6 +100,33 @@ class DisplayedFields(QAbstractListModel): # {{{ # }}} +class Background(QWidget): # {{{ + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.bcol = QColor(*gprefs['cover_grid_color']) + self.btex = gprefs['cover_grid_texture'] + self.update_brush() + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + def update_brush(self): + self.brush = QBrush(self.bcol) + if self.btex: + from calibre.gui2.preferences.texture_chooser import texture_path + path = texture_path(self.btex) + if path: + self.brush.setTexture(QPixmap(path)) + self.update() + + def sizeHint(self): + return QSize(200, 120) + + def paintEvent(self, ev): + painter = QPainter(self) + painter.fillRect(ev.rect(), self.brush) + painter.end() +# }}} + class ConfigWidget(ConfigWidgetBase, Ui_Form): size_calculated = pyqtSignal(object) @@ -209,10 +238,24 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): keys = [unicode(x.toString(QKeySequence.NativeText)) for x in keys] self.fs_help_msg.setText(unicode(self.fs_help_msg.text())%( _(' or ').join(keys))) - self.cover_grid_color_button.clicked.connect(self.change_cover_grid_color) - self.cover_grid_default_color_button.clicked.connect(self.restore_cover_grid_color) self.size_calculated.connect(self.update_cg_cache_size, type=Qt.QueuedConnection) self.tabWidget.currentChanged.connect(self.tab_changed) + + l = self.cg_background_box.layout() + self.cg_bg_widget = w = Background(self) + l.addWidget(w, 0, 0, 3, 1) + self.cover_grid_color_button = b = QPushButton(_('Change &color'), self) + b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + l.addWidget(b, 0, 1) + b.clicked.connect(self.change_cover_grid_color) + self.cover_grid_texture_button = b = QPushButton(_('Change &background image'), self) + b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + l.addWidget(b, 1, 1) + b.clicked.connect(self.change_cover_grid_texture) + self.cover_grid_default_appearance_button = b = QPushButton(_('Restore &default appearance'), self) + b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + l.addWidget(b, 2, 1) + b.clicked.connect(self.restore_cover_grid_appearance) self.cover_grid_empty_cache.clicked.connect(self.empty_cache) self.cover_grid_open_cache.clicked.connect(self.open_cg_cache) self.cover_grid_smaller_cover.clicked.connect(partial(self.resize_cover, True)) @@ -270,6 +313,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.edit_rules.initialize(db.field_metadata, db.prefs, mi, 'column_color_rules') self.icon_rules.initialize(db.field_metadata, db.prefs, mi, 'column_icon_rules') self.set_cg_color(gprefs['cover_grid_color']) + self.set_cg_texture(gprefs['cover_grid_texture']) self.update_aspect_ratio() def open_cg_cache(self): @@ -292,9 +336,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.size_calculated.emit(self.gui.grid_view.thumbnail_cache.current_size) def set_cg_color(self, val): - pal = QPalette() - pal.setColor(QPalette.Window, QColor(*val)) - self.cover_grid_color_label.setPalette(pal) + self.cg_bg_widget.bcol = QColor(*val) + self.cg_bg_widget.update_brush() + + def set_cg_texture(self, val): + self.cg_bg_widget.btex = val + self.cg_bg_widget.update_brush() def empty_cache(self): self.gui.grid_view.thumbnail_cache.empty() @@ -312,17 +359,32 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.icon_rules.clear() self.changed_signal.emit() self.set_cg_color(gprefs.defaults['cover_grid_color']) + self.set_cg_texture(gprefs.defaults['cover_grid_texture']) def change_cover_grid_color(self): - col = QColorDialog.getColor(self.cover_grid_color_label.palette().color(QPalette.Window), + col = QColorDialog.getColor(self.cg_bg_widget.bcol, self.gui, _('Choose background color for cover grid')) if col.isValid(): col = tuple(col.getRgb())[:3] self.set_cg_color(col) self.changed_signal.emit() + if self.cg_bg_widget.btex: + if question_dialog( + self, _('Remove background image?'), + _('There is currently a background image set, so the color' + ' you have chosen will not be visible. Remove the background image?')): + self.set_cg_texture(None) - def restore_cover_grid_color(self): + def change_cover_grid_texture(self): + from calibre.gui2.preferences.texture_chooser import TextureChooser + d = TextureChooser(parent=self, initial=self.cg_bg_widget.btex) + if d.exec_() == d.Accepted: + self.set_cg_texture(d.texture) + self.changed_signal.emit() + + def restore_cover_grid_appearance(self): self.set_cg_color(gprefs.defaults['cover_grid_color']) + self.set_cg_texture(gprefs.defaults['cover_grid_texture']) self.changed_signal.emit() def build_font_obj(self): @@ -383,7 +445,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.display_model.commit() self.edit_rules.commit(self.gui.current_db.prefs) self.icon_rules.commit(self.gui.current_db.prefs) - gprefs['cover_grid_color'] = tuple(self.cover_grid_color_label.palette().color(QPalette.Window).getRgb())[:3] + gprefs['cover_grid_color'] = tuple(self.cg_bg_widget.bcol.getRgb())[:3] + gprefs['cover_grid_texture'] = self.cg_bg_widget.btex return rr def refresh_gui(self, gui): diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 774c017698..af09d6d50d 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -312,61 +312,12 @@ - - - - - Background color for the cover grid: - - - cover_grid_color_button - - - - - - - - 50 - 50 - - - - true - - - - - - - - - - Change &color - - - - - - - Restore &default color - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + Background for the cover grid + + + diff --git a/src/calibre/gui2/preferences/texture_chooser.py b/src/calibre/gui2/preferences/texture_chooser.py new file mode 100644 index 0000000000..c48d274759 --- /dev/null +++ b/src/calibre/gui2/preferences/texture_chooser.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2013, Kovid Goyal ' + +import glob, os, string, shutil +from functools import partial +from PyQt4.Qt import ( + QDialog, QVBoxLayout, QListWidget, QListWidgetItem, Qt, QIcon, + QApplication, QSize, QPixmap, QDialogButtonBox, QTimer) + +from calibre.constants import config_dir +from calibre.gui2 import choose_files, error_dialog +from calibre.utils.icu import sort_key + +def texture_dir(): + ans = os.path.join(config_dir, 'textures') + if not os.path.exists(ans): + os.makedirs(ans) + return ans + +def texture_path(fname): + if not fname: + return + if fname.startswith(':'): + return I('textures/%s' % fname[1:]) + return os.path.join(texture_dir(), fname) + +class TextureChooser(QDialog): + + def __init__(self, parent=None, initial=None): + QDialog.__init__(self, parent) + self.setWindowTitle(_('Choose a texture')) + + self.l = l = QVBoxLayout() + self.setLayout(l) + + self.tdir = texture_dir() + + self.images = il = QListWidget(self) + il.itemDoubleClicked.connect(self.accept, type=Qt.QueuedConnection) + il.setIconSize(QSize(256, 256)) + il.setViewMode(il.IconMode) + il.setFlow(il.LeftToRight) + il.setSpacing(20) + il.setSelectionMode(il.SingleSelection) + il.itemSelectionChanged.connect(self.update_remove_state) + l.addWidget(il) + + self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + b = self.add_button = bb.addButton(_('Add texture'), bb.ActionRole) + b.setIcon(QIcon(I('plus.png'))) + b.clicked.connect(self.add_texture) + b = self.remove_button = bb.addButton(_('Remove texture'), bb.ActionRole) + b.setIcon(QIcon(I('minus.png'))) + b.clicked.connect(self.remove_texture) + l.addWidget(bb) + + images = [{ + 'fname': ':'+os.path.basename(x), + 'path': x, + 'name': ' '.join(map(string.capitalize, os.path.splitext(os.path.basename(x))[0].split('_'))) + } for x in glob.glob(I('textures/*.png'))] + [{ + 'fname': os.path.basename(x), + 'path': x, + 'name': os.path.splitext(os.path.basename(x))[0], + } for x in glob.glob(os.path.join(self.tdir, '*')) if x.rpartition('.')[-1].lower() in {'jpeg', 'png', 'jpg'}] + + images.sort(key=lambda x:sort_key(x['name'])) + + map(self.create_item, images) + self.update_remove_state() + + if initial: + existing = {unicode(i.data(Qt.UserRole).toString()):i for i in (self.images.item(c) for c in xrange(self.images.count()))} + item = existing.get(initial, None) + if item is not None: + item.setSelected(True) + QTimer.singleShot(100, partial(il.scrollToItem, item)) + + self.resize(QSize(950, 650)) + + def create_item(self, data): + x = data + i = QListWidgetItem(QIcon(QPixmap(x['path']).scaled(256, 256, transformMode=Qt.SmoothTransformation)), x['name'], self.images) + i.setData(Qt.UserRole, x['fname']) + i.setData(Qt.UserRole+1, x['path']) + return i + + def update_remove_state(self): + removeable = bool(self.selected_fname and not self.selected_fname.startswith(':')) + self.remove_button.setEnabled(removeable) + + @property + def texture(self): + return self.selected_fname + + def add_texture(self): + path = choose_files(self, 'choose-texture-image', _('Choose Image'), + filters=[(_('Images'), ['jpeg', 'jpg', 'png'])], all_files=False, select_only_single_file=True) + if not path: + return + path = path[0] + fname = os.path.basename(path) + name = fname.rpartition('.')[0] + existing = {unicode(i.data(Qt.UserRole).toString()):i for i in (self.images.item(c) for c in xrange(self.images.count()))} + dest = os.path.join(self.tdir, fname) + with open(path, 'rb') as s, open(dest, 'wb') as f: + shutil.copyfileobj(s, f) + if fname in existing: + self.takeItem(existing[fname]) + data = {'fname': fname, 'path': dest, 'name': name} + i = self.create_item(data) + i.setSelected(True) + self.images.scrollToItem(i) + + @property + def selected_item(self): + for x in self.images.selectedItems(): + return x + + @property + def selected_fname(self): + try: + return unicode(self.selected_item.data(Qt.UserRole).toString()) + except (AttributeError, TypeError): + pass + + def remove_texture(self): + if not self.selected_fname: + return + if self.selected_fname.startswith(':'): + return error_dialog(self, _('Cannot remove'), + _('Cannot remover builtin textures'), show=True) + self.images.takeItem(self.images.row(self.selected_item)) + +if __name__ == '__main__': + app = QApplication([]) # noqa + d = TextureChooser() + d.exec_() + print (d.texture)