From b1652a23163c76755d2c84dedbbdcc07c824e690 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 17 Nov 2013 12:11:38 +0530 Subject: [PATCH] Implement adding files to the book --- imgsrc/document-new.svg | 602 ++++++++++++++++++ resources/images/document-new.png | Bin 0 -> 7273 bytes src/calibre/ebooks/oeb/polish/container.py | 40 ++ .../ebooks/oeb/polish/tests/container.py | 17 + src/calibre/gui2/tweak_book/boss.py | 60 +- src/calibre/gui2/tweak_book/editor/text.py | 5 +- src/calibre/gui2/tweak_book/editor/widget.py | 3 + src/calibre/gui2/tweak_book/file_list.py | 99 ++- src/calibre/gui2/tweak_book/templates.py | 44 ++ src/calibre/gui2/tweak_book/ui.py | 13 +- 10 files changed, 868 insertions(+), 15 deletions(-) create mode 100644 imgsrc/document-new.svg create mode 100644 resources/images/document-new.png create mode 100644 src/calibre/gui2/tweak_book/templates.py diff --git a/imgsrc/document-new.svg b/imgsrc/document-new.svg new file mode 100644 index 0000000000..8b9f1a8ab5 --- /dev/null +++ b/imgsrc/document-new.svg @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/document-new.png b/resources/images/document-new.png new file mode 100644 index 0000000000000000000000000000000000000000..e9b3cb61b960ce160b27f92cd315c1044f7ed821 GIT binary patch literal 7273 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' + msg) + return False + + def import_file(self): + path = choose_files(self, 'tweak-book-new-resource-file', _('Choose file'), select_only_single_file=True) + if path: + path = path[0] + with open(path, 'rb') as f: + self.file_data = f.read() + name = os.path.basename(path) + self.name.setText(name) + + @property + def name_is_ok(self): + name = unicode(self.name.text()) + if not name or not name.strip(): + return self.show_error('') + ext = name.rpartition('.')[-1] + if not ext or ext == name: + return self.show_error(_('The file name must have an extension')) + norm = name.replace('\\', '/') + parts = name.split('/') + for x in parts: + if sanitize_file_name_unicode(x) != x: + return self.show_error(_('The file name contains invalid characters')) + if current_container().has_name(norm): + return self.show_error(_('This file name already exists in the book')) + self.show_error('') + return True + + def update_ok(self, *args): + self.ok_button.setEnabled(self.name_is_ok) + + def accept(self): + if not self.name_is_ok: + return error_dialog(self, _('No name specified'), _( + 'You must specify a name for the new file, with an extension, for example, chapter1.html'), show=True) + name = unicode(self.name.text()) + name, ext = name.rpartition('.')[0::2] + name = (name + '.' + ext.lower()).replace('\\', '/') + mt = guess_type(name) + if mt in OEB_DOCS: + self.file_data = template_for('html').encode('utf-8') + self.using_template = True + elif mt in OEB_STYLES: + self.file_data = template_for('css').encode('utf-8') + self.using_template = True + self.file_name = name + QDialog.accept(self) + class FileListWidget(QWidget): delete_requested = pyqtSignal(object, object) @@ -375,7 +466,7 @@ class FileListWidget(QWidget): self.layout().setContentsMargins(0, 0, 0, 0) for x in ('delete_requested', 'reorder_spine', 'rename_requested', 'edit_file'): getattr(self.file_list, x).connect(getattr(self, x)) - for x in ('delete_done',): + for x in ('delete_done', 'select_name'): setattr(self, x, getattr(self.file_list, x)) def build(self, container, preserve_state=True): diff --git a/src/calibre/gui2/tweak_book/templates.py b/src/calibre/gui2/tweak_book/templates.py new file mode 100644 index 0000000000..5024819bb5 --- /dev/null +++ b/src/calibre/gui2/tweak_book/templates.py @@ -0,0 +1,44 @@ +#!/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 ' + +from calibre import prepare_string_for_xml +from calibre.gui2.tweak_book import current_container + +DEFAULT_TEMPLATES = { + 'html': +'''\ + + + + {TITLE} + + + %CURSOR% + + +''', + + + 'css': +'''\ +@charset utf-8; +/* Styles for {TITLE} */ +%CURSOR% +''', + +} + +def template_for(syntax): + mi = current_container().mi + data = { + 'TITLE':mi.title, + 'AUTHOR': ' & '.join(mi.authors), + } + template = DEFAULT_TEMPLATES.get(syntax, '') + return template.format(**{k:prepare_string_for_xml(v, True) for k, v in data.iteritems()}) + diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index fb51ddd56d..822f6539f2 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -9,7 +9,7 @@ __copyright__ = '2013, Kovid Goyal ' from functools import partial from PyQt4.Qt import ( - QDockWidget, Qt, QLabel, QIcon, QAction, QApplication, QWidget, + QDockWidget, Qt, QLabel, QIcon, QAction, QApplication, QWidget, QFontMetrics, QVBoxLayout, QStackedWidget, QTabWidget, QImage, QPixmap, pyqtSignal) from calibre.constants import __appname__, get_version @@ -22,6 +22,10 @@ from calibre.gui2.tweak_book.keyboard import KeyboardManager from calibre.gui2.tweak_book.preview import Preview from calibre.gui2.tweak_book.search import SearchPanel +def elided_text(font, text, width=200, mode=Qt.ElideMiddle): + fm = QFontMetrics(font) + return unicode(fm.elidedText(text, mode, int(width))) + class Central(QStackedWidget): ' The central widget, hosts the editors ' @@ -146,6 +150,9 @@ class Main(MainWindow): self.keyboard.finalize() self.keyboard.set_mode('other') + def elided_text(self, text, width=200, mode=Qt.ElideMiddle): + return elided_text(self.font(), text, width=width, mode=mode) + @property def editor_tabs(self): return self.central.editor_tabs @@ -165,6 +172,7 @@ class Main(MainWindow): self.addAction(ac) return ac + self.action_new_file = reg('document-new.png', _('&New file'), self.boss.add_file, 'new-file', (), _('Create a new file in the current book')) self.action_open_book = reg('document_open.png', _('Open &book'), self.boss.open_book, 'open-book', 'Ctrl+O', _('Open a new book')) self.action_global_undo = reg('back.png', _('&Revert to before'), self.boss.do_global_undo, 'global-undo', 'Ctrl+Left', _('Revert book to before the last action (Undo)')) @@ -245,6 +253,7 @@ class Main(MainWindow): b = self.menuBar() f = b.addMenu(_('&File')) + f.addAction(self.action_new_file) f.addAction(self.action_open_book) f.addAction(self.action_save) f.addAction(self.action_quit) @@ -302,7 +311,7 @@ class Main(MainWindow): return b a = create(_('Book tool bar'), 'global').addAction - for x in ('open_book', 'global_undo', 'global_redo', 'save', 'create_checkpoint', 'toc'): + for x in ('new_file', 'open_book', 'global_undo', 'global_redo', 'save', 'create_checkpoint', 'toc'): a(getattr(self, 'action_' + x)) a = create(_('Polish book tool bar'), 'polish').addAction