10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
107 |
108 |
109 |
110 |
11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
111 |
112 |
113 |
114 |
12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
115 |
116 |
117 |
118 |
13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
127 |
128 |
129 |
130 |
16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
131 |
132 |
133 |
134 |
18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
145 |
146 |
147 |
24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
10.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
167 |
168 |
169 |
170 |
11.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
171 |
172 |
173 |
174 |
12.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
175 |
176 |
177 |
178 |
13.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
14.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
188 |
189 |
190 |
191 |
16.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
192 |
193 |
194 |
195 |
18.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
20.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
205 |
206 |
207 |
24.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
30.Aenean lacinia bibendum nulla sed consectetur. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue.
Donec sed odio dui. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
241 |
242 |
243 |
Pellentesque ornare sem
244 |
245 |
Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Donec ullamcorper nulla non metus auctor fringilla. Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam id dolor id nibh ultricies vehicula ut id elit.
246 |
247 |
Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
248 |
249 |
Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Aenean lacinia bibendum nulla sed consectetur.
250 |
251 |
Nullam quis risus eget urna mollis ornare vel eu leo. Nullam quis risus eget urna mollis ornare vel eu leo. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus auctor fringilla.
252 |
253 |
Cras mattis consectetur
254 |
255 |
Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Aenean lacinia bibendum nulla sed consectetur. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cras mattis consectetur purus sit amet fermentum.
256 |
257 |
Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam quis risus eget urna mollis ornare vel eu leo. Cras mattis consectetur purus sit amet fermentum.
258 |
259 |
260 |
261 |
262 |
263 |
Nullam quis risus eget urna mollis ornare vel eu leo. Donec ullamcorper nulla non metus auctor fringilla. Cras mattis consectetur purus sit amet fermentum. Sed posuere consectetur est at lobortis. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
264 |
265 |
266 |
Maecenas sed diam eget risus varius.
267 |
268 |
Vestibulum id ligula porta felis euismod semper. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Vestibulum id ligula porta felis euismod semper. Sed posuere consectetur est at lobortis. Maecenas sed diam eget risus varius blandit sit amet non magna. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
269 |
270 |
271 |
272 |
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Aenean lacinia bibendum nulla sed consectetur. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Aenean lacinia bibendum nulla sed consectetur. Nullam quis risus eget urna mollis ornare vel eu leo.
273 |
274 |
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec ullamcorper nulla non metus auctor fringilla. Maecenas faucibus mollis interdum. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
Language Support
291 |
The subset of Inconsolata Medium in this kit supports the following languages:
292 |
293 | Albanian, Basque, Breton, Chamorro, Danish, Dutch, English, Faroese, Finnish, French, Frisian, Galician, German, Icelandic, Italian, Malagasy, Norwegian, Portuguese, Spanish, Swedish
294 |
Glyph Chart
295 |
The subset of Inconsolata Medium in this kit includes all the glyphs listed below. Unicode entities are included above each glyph to help you insert individual characters into your layout.
Webfonts are supported by all major browser platforms but not all in the same way. There are currently four different font formats that must be included in order to target all browsers. This includes TTF, WOFF, EOT and SVG.
543 |
544 |
1. Upload your webfonts
545 |
You must upload your webfont kit to your website. They should be in or near the same directory as your CSS files.
546 |
547 |
2. Include the webfont stylesheet
548 |
A special CSS @font-face declaration helps the various browsers select the appropriate font it needs without causing you a bunch of headaches. Learn more about this syntax by reading the Fontspring blog post about it. The code for it is as follows:
To take advantage of your new fonts, you must tell your stylesheet to use them. Look at the original @font-face declaration above and find the property called "font-family." The name linked there will be what you use to reference the font. Prepend that webfont name to the font stack in the "font-family" property, inside the selector you want to change. For example:
Getting webfonts to work cross-browser can be tricky. Use the information in the sidebar to help you if you find that fonts aren't loading in a particular browser.
571 |
572 |
573 |
574 |
575 |
Troubleshooting Font-Face Problems
576 |
Having trouble getting your webfonts to load in your new website? Here are some tips to sort out what might be the problem.
577 |
578 |
Fonts not showing in any browser
579 |
580 |
This sounds like you need to work on the plumbing. You either did not upload the fonts to the correct directory, or you did not link the fonts properly in the CSS. If you've confirmed that all this is correct and you still have a problem, take a look at your .htaccess file and see if requests are getting intercepted.
581 |
582 |
Fonts not loading in iPhone or iPad
583 |
584 |
The most common problem here is that you are serving the fonts from an IIS server. IIS refuses to serve files that have unknown MIME types. If that is the case, you must set the MIME type for SVG to "image/svg+xml" in the server settings. Follow these instructions from Microsoft if you need help.
585 |
586 |
Fonts not loading in Firefox
587 |
588 |
The primary reason for this failure? You are still using a version Firefox older than 3.5. So upgrade already! If that isn't it, then you are very likely serving fonts from a different domain. Firefox requires that all font assets be served from the same domain. Lastly it is possible that you need to add WOFF to your list of MIME types (if you are serving via IIS.)
589 |
590 |
Fonts not loading in IE
591 |
592 |
Are you looking at Internet Explorer on an actual Windows machine or are you cheating by using a service like Adobe BrowserLab? Many of these screenshot services do not render @font-face for IE. Best to test it on a real machine.
593 |
594 |
Fonts not loading in IE9
595 |
596 |
IE9, like Firefox, requires that fonts be served from the same domain as the website. Make sure that is the case.
597 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
607 |
608 |
609 |
610 |
--------------------------------------------------------------------------------
/noteorganiser/frames.py:
--------------------------------------------------------------------------------
1 | """
2 | .. module:: frames
3 | :synopsys: Define all the custom frames
4 |
5 | .. moduleauthor:: Benjamin Audren
6 | """
7 | from __future__ import unicode_literals
8 | import os
9 | import shutil
10 | from collections import OrderedDict as od
11 | import pypandoc as pa
12 | import six # Used to replace the od iteritems from py2
13 | import io
14 | import traceback # For failure display
15 | import time # for sleep
16 |
17 | from PySide import QtGui
18 | from PySide import QtCore
19 | from PySide import QtWebKit
20 |
21 | os.environ['QT_API'] = 'PySide'
22 | import qtawesome
23 |
24 | from .utils import FlowLayout
25 | from .utils import fuzzySearch
26 |
27 | from subprocess import Popen
28 |
29 | # Local imports
30 | from .popups import NewEntry, NewNotebook, NewFolder
31 | import noteorganiser.text_processing as tp
32 | from .constants import EXTENSION
33 | from .configuration import search_folder_recursively
34 | from .syntax import ModifiedMarkdownHighlighter
35 | from .widgets import PicButton, VerticalScrollArea, LineEditWithClearButton
36 |
37 |
38 | class CustomFrame(QtGui.QFrame):
39 | """Base class for all three tabbed frames"""
40 |
41 | def __init__(self, parent=None):
42 | """ Create the basic layout """
43 | QtGui.QFrame.__init__(self, parent)
44 | # Create a shortcut notation for the main information
45 | self.parent = parent
46 | self.info = parent.info
47 | self.log = parent.log
48 |
49 | # Create the main layout
50 | self.setLayout(QtGui.QVBoxLayout())
51 |
52 | if hasattr(self, 'initLogic'):
53 | self.initLogic()
54 |
55 | self.initUI()
56 |
57 | def initUI(self):
58 | """
59 | This will be called on creation
60 |
61 | A daughter class should implement this function
62 | """
63 | raise NotImplementedError
64 |
65 | def initToolBar(self):
66 | """
67 | This will initialize a toolbar in the parent window
68 |
69 | a daughter class should implement this function if it needs a toolbar.
70 |
71 | If this toolbar should only be visible, when the view is active,
72 | connect to tabs.currentChanged()
73 | Example:
74 | @QtCore.Slot(int)
75 | def showActiveToolBar(self, tabIndex):
76 | activeTab = self.tabs.tabText(tabIndex)
77 | # activate, if there's a toolbar in library / editing
78 | if activeTab == "&Library":
79 | self.library.shelves.toolbar.setVisible(True)
80 | else:
81 | self.library.shelves.toolbar.setVisible(False)
82 | if activeTab == "&Editing":
83 | self.editing.toolbar.setVisible(True)
84 | else:
85 | self.editing.toolbar.setVisible(False)
86 | if activeTab == "Previe&w":
87 | self.preview.toolbar.setVisible(True)
88 | else:
89 | self.preview.toolbar.setVisible(False)
90 | """
91 | raise NotImplementedError
92 |
93 | def clearUI(self):
94 | """ Common method for recursively cleaning layouts """
95 | while self.layout().count():
96 | item = self.layout().takeAt(0)
97 | if isinstance(item, QtGui.QLayout):
98 | self.clearLayout(item)
99 | item.deleteLater()
100 | else:
101 | try:
102 | widget = item.widget()
103 | if widget is not None:
104 | widget.deleteLater()
105 | except AttributeError:
106 | pass
107 |
108 | def clearLayout(self, layout):
109 | """ Submethod to help cleaning the UI before redrawing """
110 | if layout is not None:
111 | while layout.count():
112 | item = layout.takeAt(0)
113 | widget = item.widget()
114 | if widget is not None:
115 | widget.deleteLater()
116 | else:
117 | self.clearLayout(item.layout())
118 |
119 | def zoomIn(self):
120 | raise NotImplementedError
121 |
122 | def zoomOut(self):
123 | raise NotImplementedError
124 |
125 | def resetSize(self):
126 | raise NotImplementedError
127 |
128 |
129 | class Library(CustomFrame):
130 | r"""
131 | The notebooks will be stored and displayed there
132 |
133 | Should ressemble something like this:
134 | _________ _________ _________
135 | / Library \/ Editing \/ Preview \
136 | | ----------------------------------
137 | | | global tag |
138 | | notebook_1 notebook_2 | another tag|
139 | | ------------------------------ tag taggy |
140 | | | taggy tag |
141 | | notebook_3 | |
142 | | | |
143 | | [up] [new N] [new F] | |
144 | --------------------------------------------|
145 | """
146 | def initUI(self):
147 | self.log.info("Starting UI init of %s" % self.__class__.__name__)
148 |
149 | # Create the shelves object
150 | self.shelves = Shelves(self)
151 | self.layout().addWidget(self.shelves)
152 |
153 | # toolbar on top
154 | self.initToolBar()
155 |
156 | # right click in empty space
157 | self.initContextMenu()
158 |
159 | self.log.info("Finished UI init of %s" % self.__class__.__name__)
160 |
161 | def initToolBar(self):
162 | """initialize the toolbar for this view"""
163 | if not hasattr(self, 'toolbar'):
164 | self.toolbar = self.parent.addToolBar('Library')
165 | self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
166 | self.toolbar.setIconSize(self.toolbar.iconSize() * 0.7)
167 |
168 | # Go up in the directories (disabled if in the root directory)
169 | upIcon = qtawesome.icon('fa.arrow-up')
170 | self.upAction = QtGui.QAction(upIcon, '&Up', self)
171 | self.upAction.setIconText('&Up')
172 | self.upAction.setShortcut('Ctrl+U')
173 | self.upAction.triggered.connect(self.shelves.upFolder)
174 | if self.info.level == self.info.root:
175 | self.upAction.setDisabled(True)
176 | self.toolbar.addAction(self.upAction)
177 |
178 | # Create a new notebook
179 | newNotebookIcon = qtawesome.icon('fa.file')
180 | self.newNotebookAction = QtGui.QAction(newNotebookIcon,
181 | '&New Notebook', self)
182 | self.newNotebookAction.setIconText('&New Notebook')
183 | self.newNotebookAction.setShortcut('Ctrl+N')
184 | self.newNotebookAction.triggered.connect(
185 | self.shelves.createNotebook)
186 | self.toolbar.addAction(self.newNotebookAction)
187 |
188 | # Create a new folder
189 | newFolderIcon = qtawesome.icon('fa.folder')
190 | self.newFolderAction = QtGui.QAction(newFolderIcon, 'New Folde&r',
191 | self)
192 | self.newFolderAction.setIconText('New Folde&r')
193 | self.newFolderAction.setShortcut('Ctrl+F')
194 | self.newFolderAction.triggered.connect(self.shelves.createFolder)
195 | self.toolbar.addAction(self.newFolderAction)
196 |
197 | def initContextMenu(self):
198 | """
199 | add actions to the context menu of the library itself
200 | (the empty space)
201 | this reuses the actions from initToolBar()
202 | """
203 |
204 | self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
205 | self.addAction(self.newNotebookAction)
206 | self.addAction(self.newFolderAction)
207 |
208 | def refresh(self):
209 | """ Refresh all elements of the frame """
210 | self.shelves.refresh()
211 |
212 |
213 | class Editing(CustomFrame):
214 | r"""
215 | Direct access to the markup files will be there
216 |
217 | The left hand side will be the text within a tab widget, named as the
218 | notebook it belongs to.
219 |
220 | Contrary to the Library tab, this one will have an additional state, the
221 | active state, which will dictate on which file the window is open.
222 |
223 | _________ _________ _________
224 | / Library \/ Editing \/ Preview \
225 | |---------- ----------------------------
226 | | --------------------------| |
227 | | /| | [+] new entry |
228 | | N| | [ ] save document|
229 | | 1| | [ ] preview |
230 | | \|_________________________| |
231 | ---------------------------------------------------
232 | """
233 | # Launched when the previewer is desired
234 | loadNotebook = QtCore.Signal(str)
235 |
236 | def initUI(self):
237 | self.log.info("Starting UI init of %s" % self.__class__.__name__)
238 |
239 | # toolbar on top
240 | self.initToolBar()
241 |
242 | # Global horizontal layout
243 | hbox = QtGui.QHBoxLayout()
244 |
245 | # Create the tabbed widgets containing the text editors. The tabs will
246 | # appear on the left-hand side
247 | self.tabs = QtGui.QTabWidget(self)
248 | self.tabs.setTabPosition(QtGui.QTabWidget.West)
249 |
250 | # The loop is over all the notebooks in the **current** folder
251 | for notebook in self.info.notebooks:
252 | editor = TextEditor(self)
253 | # Set the source of the TextEditor to the desired notebook
254 | editor.setSource(os.path.join(self.info.level, notebook))
255 | # Add the text editor to the tabbed area
256 | self.tabs.addTab(editor, os.path.splitext(notebook)[0])
257 |
258 | hbox.addWidget(self.tabs)
259 | self.layout().addLayout(hbox)
260 |
261 | self.log.info("Finished UI init of %s" % self.__class__.__name__)
262 |
263 | def initToolBar(self):
264 | """initialize the toolbar for this view"""
265 | if not hasattr(self, 'toolbar'):
266 | self.toolbar = self.parent.addToolBar('Editing')
267 | self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
268 | self.toolbar.setIconSize(self.toolbar.iconSize() * 0.7)
269 | self.toolbar.setVisible(False)
270 |
271 | # save the Text in the current notebook editor
272 | saveIcon = qtawesome.icon('fa.floppy-o')
273 | self.saveAction = QtGui.QAction(saveIcon, '&Save', self)
274 | self.saveAction.setIconText('&Save')
275 | self.saveAction.setShortcut('Ctrl+S')
276 | self.saveAction.triggered.connect(self.saveText)
277 | self.toolbar.addAction(self.saveAction)
278 |
279 | # reload the Text in the current notebook editor
280 | readIcon = qtawesome.icon('fa.refresh')
281 | self.readAction = QtGui.QAction(readIcon, '&Reload', self)
282 | self.readAction.setIconText('&Reload')
283 | self.readAction.setShortcut('Ctrl+R')
284 | self.readAction.triggered.connect(self.loadText)
285 | self.toolbar.addAction(self.readAction)
286 |
287 | # separator between general and notebook specific actions
288 | self.toolbar.addSeparator()
289 |
290 | # Create a new entry - new field in the current notebook
291 | newEntryIcon = qtawesome.icon('fa.plus-square')
292 | self.newEntryAction = QtGui.QAction(newEntryIcon, '&New entry',
293 | self)
294 | self.newEntryAction.setIconText('&New entry')
295 | self.newEntryAction.setShortcut('Ctrl+N')
296 | self.newEntryAction.triggered.connect(self.newEntry)
297 | self.toolbar.addAction(self.newEntryAction)
298 |
299 | # Edit in an exterior editor
300 | editIcon = qtawesome.icon('fa.pencil-square-o')
301 | self.editAction = QtGui.QAction(editIcon,
302 | 'Edi&t (exterior editor)', self)
303 | self.editAction.setIconText('Edi&t (exterior editor)')
304 | self.editAction.setShortcut('Ctrl+T')
305 | self.editAction.triggered.connect(self.editExternal)
306 | self.toolbar.addAction(self.editAction)
307 |
308 | # Launch the previewing of the current notebook
309 | previewIcon = qtawesome.icon('fa.desktop')
310 | self.previewAction = QtGui.QAction(previewIcon,
311 | '&Preview notebook', self)
312 | self.previewAction.setIconText('&Preview notebook')
313 | self.previewAction.setShortcut('Ctrl+P')
314 | self.previewAction.triggered.connect(self.preview)
315 | self.toolbar.addAction(self.previewAction)
316 |
317 | # open file dialog to insert an image path
318 | imageInsertIcon = qtawesome.icon('fa.image')
319 | self.imageInsertAction = QtGui.QAction(imageInsertIcon,
320 | '&Insert Image', self)
321 | self.imageInsertAction.setIconText('Insert Image')
322 | self.imageInsertAction.setShortcut('Ctrl+I')
323 | self.imageInsertAction.triggered.connect(self.insertImage)
324 | self.toolbar.addAction(self.imageInsertAction)
325 |
326 | def refresh(self):
327 | """Redraw the UI (time consuming...)"""
328 | self.clearUI()
329 | self.initUI()
330 |
331 | def switchNotebook(self, notebook):
332 | """switching tab to desired notebook"""
333 | self.log.info("switching to "+notebook)
334 | index = self.info.notebooks.index(notebook+EXTENSION)
335 | self.tabs.setCurrentIndex(index)
336 |
337 | def newEntry(self):
338 | """
339 | Open a form and store the results to the file
340 |
341 | .. note::
342 | this method does not save the file automatically
343 |
344 | """
345 | self.popup = NewEntry(self)
346 | # This will popup the popup
347 | ok = self.popup.exec_()
348 | # The return code is True if successful
349 | if ok:
350 | # Recover the three fields
351 | title = self.popup.title
352 | tags = self.popup.tags
353 | corpus = self.popup.corpus
354 |
355 | # Create the post
356 | post = tp.create_post_from_entry(title, tags, corpus)
357 | # recover the editor of the current widget, i.e. the open editor
358 | editor = self.tabs.currentWidget()
359 | # Append the text
360 | editor.appendText(post)
361 |
362 | def editExternal(self): # pragma: no cover
363 | """edit active file in external editor"""
364 | # get the current file
365 | index = self.tabs.currentIndex()
366 | notebook = os.path.join(self.info.level, self.info.notebooks[index])
367 | # open the file in the external editor set by the user
368 | # if this fails, show a popup
369 | try:
370 | Popen([self.info.externalEditor, notebook])
371 | self.log.info('external editor opened for notebook %s' % notebook)
372 | except OSError as e:
373 | self.log.error('Execution of external editor failed: %s' % e)
374 | self.popup = QtGui.QMessageBox(self)
375 | self.popup.setIcon(QtGui.QMessageBox.Critical)
376 | self.popup.setWindowTitle('NoteOrganiser')
377 | self.popup.setText(
378 | "The external editor '%s' couldn't be opened." % (
379 | self.info.externalEditor))
380 | self.popup.setInformativeText("%s" % e)
381 | self.popup.exec_()
382 |
383 | def preview(self):
384 | """
385 | Launch the previewing of the current notebook
386 |
387 | Fires the loadNotebook signal with the desired notebook as an
388 | argument.
389 | """
390 | index = self.tabs.currentIndex()
391 | notebook = self.info.notebooks[index]
392 | self.log.info('ask to preview notebook %s' % notebook)
393 | self.loadNotebook.emit(notebook)
394 |
395 | def zoomIn(self):
396 | """
397 | So far only applies to the inside editor, and not the global fonts
398 |
399 | """
400 | # recover the current editor
401 | editor = self.tabs.currentWidget()
402 | editor.zoomIn()
403 |
404 | def zoomOut(self):
405 | # recover the current editor
406 | editor = self.tabs.currentWidget()
407 | editor.zoomOut()
408 |
409 | def resetSize(self):
410 | # recover the current editor
411 | editor = self.tabs.currentWidget()
412 | editor.resetSize()
413 |
414 | def loadText(self):
415 | """reload the text in the current notebook"""
416 | notebook = self.tabs.currentWidget()
417 | notebook.loadText()
418 |
419 | def saveText(self):
420 | """save the text in the current notebook"""
421 | notebook = self.tabs.currentWidget()
422 | notebook.saveText()
423 |
424 | def insertImage(self):
425 | """
426 | Open a file dialog and insert the selected image path as markdown
427 | """
428 | self.popup = QtGui.QFileDialog()
429 | filename = self.popup.getOpenFileName(self,
430 | "select an image",
431 | "",
432 | "Image Files (*.png *.jpg *.bmp *.jpeg *.svg *.gif)" + \
433 | ";;all files (*.*)")
434 |
435 | # QFileDialog returns a tuple with filename and used filter
436 | if filename[0]:
437 | imagemarkdown = tp.create_image_markdown(filename[0])
438 | editor = self.tabs.currentWidget()
439 | editor.insertText(imagemarkdown)
440 |
441 |
442 | class Preview(CustomFrame):
443 | r"""
444 | Preview of the markdown in html, with tag selection
445 |
446 | The left hand side will be an html window, displaying the whole notebook.
447 | On the right, a list of tags will be displayed.
448 | At some point, a calendar for date selection should also be displayed TODO
449 |
450 | _________ _________ _________
451 | / Library \/ Editing \/ Preview \
452 | |--------------------- ------------------
453 | | --------------------------| |
454 | | | | TAG1 TAG2 tag3 |
455 | | | | tag4 ... |
456 | | | | |
457 | | |_________________________| Calendar |
458 | ---------------------------------------------------
459 | """
460 | # Launched when the editor is desired after failed conversion
461 | loadEditor = QtCore.Signal(str, str)
462 |
463 | def initLogic(self):
464 | """
465 | Create variables for storing local information
466 |
467 | """
468 | # Where to store the produced html pages
469 | self.website_root = os.path.join(self.info.level, '.website')
470 | # Where to store the temporary markdown files (maybe this step is not
471 | # necessary with pypandoc?)
472 | self.temp_root = os.path.join(self.info.level, '.temp')
473 | # Create the two folders if they do not already exist
474 | for path in (self.website_root, self.temp_root):
475 | if not os.path.isdir(path):
476 | os.mkdir(path)
477 | self.extracted_tags = od()
478 | self.filters = []
479 |
480 | # Shortcuts for resizing
481 | acceptShortcut = QtGui.QShortcut(
482 | QtGui.QKeySequence(self.tr("Ctrl+k")), self)
483 | acceptShortcut.activated.connect(self.zoomIn)
484 |
485 | def initUI(self):
486 | self.log.info("Starting UI init of %s" % self.__class__.__name__)
487 | self.layout().setDirection(QtGui.QBoxLayout.LeftToRight)
488 |
489 | # toolbar on top
490 | self.initToolBar()
491 |
492 | # Left hand side: html window
493 | self.web = QtWebKit.QWebView(self)
494 |
495 | # Set the css file. Note that the path to the css needs to be absolute,
496 | # somehow...
497 | path = os.path.abspath(os.path.dirname(__file__))
498 | self.css = os.path.join(path, 'assets', 'style', 'bootstrap.css')
499 | self.template = os.path.join(
500 | path, 'assets', 'style', 'bootstrap-blog.html')
501 | self.web.settings().setUserStyleSheetUrl(QtCore.QUrl.fromLocalFile(
502 | self.css))
503 |
504 | # The 1 stands for a stretch factor, set to 0 by default (seems to be
505 | # only for QWebView, though...
506 | self.layout().addWidget(self.web, 1)
507 |
508 | # Right hand side: Vertical layout for the tags inside a QScrollArea
509 | scrollArea = QtGui.QScrollArea()
510 | scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
511 | scrollArea.verticalScrollBar().setFocusPolicy(QtCore.Qt.StrongFocus)
512 |
513 | # Need to create a dummy Widget, because QScrollArea can not accept a
514 | # layout, only a Widget
515 | dummy = QtGui.QWidget()
516 |
517 | vbox = QtGui.QVBoxLayout()
518 | # let size grow AND shrink
519 | vbox.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize)
520 |
521 | # search field for the buttons
522 | self.searchField = LineEditWithClearButton()
523 | self.searchField.textChanged.connect(self.filterButtons)
524 | self.searchField.returnPressed.connect(self.searchFieldReturn)
525 | self.searchField.setPlaceholderText('filter tags')
526 | self.searchField.setMaximumWidth(165)
527 | vbox.addWidget(self.searchField)
528 |
529 | # create a shortcut to jump into the search field
530 | if not hasattr(self, 'searchAction'):
531 | self.searchAction = QtGui.QAction(self)
532 | self.searchAction.setShortcut('Ctrl+F')
533 | self.searchAction.triggered.connect(self.onSearchAction)
534 | self.addAction(self.searchAction)
535 |
536 | self.tagButtons = []
537 | if self.extracted_tags:
538 | for key, value in six.iteritems(self.extracted_tags):
539 | tag = QtGui.QPushButton(key)
540 | tag.setFlat(False)
541 | tag.setMinimumSize(100, 40+5*value)
542 | tag.setMaximumWidth(165)
543 | tag.setCheckable(True)
544 | tag.clicked.connect(self.addFilter)
545 | self.tagButtons.append([key, tag])
546 | vbox.addWidget(tag)
547 | # Adding everything to the scroll area
548 | dummy.setLayout(vbox)
549 | scrollArea.setWidget(dummy)
550 | # Limit its width
551 | dummy.setFixedWidth(200)
552 |
553 | self.layout().addWidget(scrollArea)
554 |
555 | # Logging
556 | self.log.info("Finished UI init of %s" % self.__class__.__name__)
557 |
558 | def initToolBar(self):
559 | """initialize the toolbar for this view"""
560 | if not hasattr(self, 'toolbar'):
561 | self.toolbar = self.parent.addToolBar('Preview')
562 | self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
563 | self.toolbar.setIconSize(self.toolbar.iconSize() * 0.7)
564 | self.toolbar.setVisible(False)
565 |
566 | # Reload Action
567 | reloadIcon = qtawesome.icon('fa.refresh')
568 | self.reloadAction = QtGui.QAction(reloadIcon, '&Reload', self)
569 | self.reloadAction.setIconText('&Reload')
570 | self.reloadAction.setShortcut('Ctrl+R')
571 | self.reloadAction.triggered.connect(self.reload)
572 | self.toolbar.addAction(self.reloadAction)
573 |
574 | def addFilter(self):
575 | """
576 | Filter out/in a certain tag
577 |
578 | From the status of the sender button, the associated tag will be
579 | added/removed from the filter.
580 |
581 | """
582 | sender = self.sender()
583 | if not sender.isFlat():
584 | if sender.isChecked():
585 | self.log.info('tag '+sender.text()+' added to the filter')
586 | self.filters.append(sender.text())
587 | else:
588 | self.log.info('tag '+sender.text()+' removed from the filter')
589 | self.filters.pop(self.filters.index(sender.text()))
590 |
591 | self.log.info("filter %s out of %s" % (
592 | ', '.join(self.filters), self.info.current_notebook))
593 | url, self.remaining_tags = self.convert(
594 | os.path.join(self.info.level, self.info.current_notebook),
595 | self.filters)
596 | # Grey out not useful buttons
597 | for key, button in self.tagButtons:
598 | if key in self.remaining_tags:
599 | self.enableButton(button)
600 | else:
601 | self.disableButton(button)
602 | self.setWebpage(url)
603 |
604 | def setWebpage(self, page):
605 | self.web.load(QtCore.QUrl.fromLocalFile(page))
606 |
607 | def loadNotebook(self, notebook):
608 | """
609 | Load a given markdown file as an html page
610 |
611 | """
612 | # TODO the dates should be recovered as well"
613 | self.initLogic()
614 | self.info.current_notebook = notebook
615 | self.log.info("Extracting markdown from %s" % notebook)
616 |
617 | try:
618 | url, tags = self.convert(
619 | os.path.join(self.info.level, notebook), ())
620 | except ValueError: # pragma: no cover
621 | self.log.error("Markdown conversion failed, aborting")
622 | return False
623 | except SyntaxError: # pragma: no cover
624 | self.log.warning("Modified Markdown syntax error, aborting")
625 | return False
626 |
627 | self.extracted_tags = tags
628 | # Finally, set the url of the web viewer to the desired page
629 | self.clearUI()
630 | self.initUI()
631 | self.setWebpage(url)
632 | return True
633 |
634 | def convert(self, path, tags):
635 | """
636 | Convert a notebook to html, with entries corresponding to the tags
637 |
638 | TODO: during the execution of this method, a check should be performed
639 | to verify if the file already exists, or maybe inside the convert
640 | function.
641 |
642 | Returns
643 | -------
644 | url : string
645 | path to the html page
646 | remaining_tags : OrderedDict
647 | dictionary of the remaining tags (the ones appearing in posts where
648 | all the selected tags where appearing, for further refinment)
649 | """
650 | # If the conversion fails, a popup should appear to inform the user
651 | # about it
652 | try:
653 | markdown, remaining_tags = tp.from_notes_to_markdown(
654 | path, input_tags=tags)
655 | except (IndexError, UnboundLocalError): # pragma: no cover
656 | self.log.error("Conversion of %s to markdown failed" % path)
657 | self.popup = QtGui.QMessageBox(self)
658 | self.popup.setIcon(QtGui.QMessageBox.Critical)
659 | self.popup.setText(
660 | "The conversion to markdown has unexpectedly failed!")
661 | self.popup.setInformativeText("%s" % traceback.format_exc())
662 | ok = self.popup.exec_()
663 | if ok:
664 | raise ValueError("The conversion of the notebook failed")
665 | except ValueError as e: # pragma: no cover
666 | self.log.warn(
667 | "There was an expected error in converting"
668 | " %s to markdown" % path)
669 | self.popup = QtGui.QMessageBox(self)
670 | self.popup.setIcon(QtGui.QMessageBox.Warning)
671 | self.popup.setText(
672 | "Oups, you (probably) did a syntax error!")
673 | self.popup.setInformativeText("%s" % e.message)
674 | ok = self.popup.exec_()
675 | if ok:
676 | raise SyntaxError("There was a syntax error")
677 |
678 | # save a temp. The basename will be modified to reflect the selection
679 | # of tags.
680 | base = os.path.basename(path)[:-len(EXTENSION)]
681 | if tags:
682 | base += '_'+'_'.join(tags)
683 | temp_path = os.path.join(self.temp_root, base+EXTENSION)
684 | self.log.debug('Creating temp file %s' % temp_path)
685 | with io.open(temp_path, 'w', encoding='utf-8') as temp:
686 | temp.write('\n'.join(markdown))
687 |
688 | # extra arguments for pandoc
689 | extra_args = ['--highlight-style', 'pygments', '-s', '-c', self.css,
690 | '--template', self.template]
691 |
692 | # use TOC if enabled
693 | if self.info.use_TOC:
694 | extra_args.append('--toc')
695 |
696 | # Apply pandoc to this markdown file, from pypandoc thin wrapper, and
697 | # recover the html
698 | html = pa.convert(temp_path, 'html', encoding='utf-8',
699 | extra_args=extra_args)
700 |
701 | # Convert the windows ending of lines to simple line breaks (\r\n to
702 | # \n)
703 | html = html.replace('\r\n', '\n')
704 |
705 | # Write the html to a file
706 | url = os.path.join(self.website_root, base+'.html')
707 | with io.open(url, 'w', encoding='utf-8') as page:
708 | page.write(html)
709 |
710 | return url, remaining_tags
711 |
712 | def disableButton(self, button):
713 | """ TODO: this should also alter the style """
714 | button.setFlat(True)
715 | button.setCheckable(False)
716 |
717 | def enableButton(self, button):
718 | """ TODO: this should also alter the style """
719 | button.setFlat(False)
720 | button.setCheckable(True)
721 |
722 | def zoomIn(self):
723 | multiplier = self.web.textSizeMultiplier()
724 | self.web.setTextSizeMultiplier(multiplier+0.1)
725 |
726 | def zoomOut(self):
727 | multiplier = self.web.textSizeMultiplier()
728 | self.web.setTextSizeMultiplier(multiplier-0.1)
729 |
730 | def resetSize(self):
731 | self.web.setTextSizeMultiplier(1)
732 |
733 | def onSearchAction(self):
734 | """Search shortcut was pressed. Set focus to the searchfield"""
735 | self.searchField.setFocus()
736 |
737 | def reload(self):
738 | """
739 | recompute and reload current html file
740 |
741 | keep currently activated filters
742 | """
743 | self.log.info('reloading the current preview')
744 | url, self.remaining_tags = self.convert(
745 | os.path.join(self.info.level, self.info.current_notebook),
746 | self.filters)
747 | for key, button in self.tagButtons:
748 | if key in self.remaining_tags:
749 | self.enableButton(button)
750 | else:
751 | self.disableButton(button)
752 | self.setWebpage(url)
753 |
754 | def filterButtons(self, filterText):
755 | """
756 | filter buttons by the text in the search field
757 |
758 | gets called when the text in the search field changes
759 | """
760 | for key, button in self.tagButtons:
761 | button.setVisible(fuzzySearch(filterText, key))
762 |
763 | def searchFieldReturn(self):
764 | """
765 | return key was pressed in the searchField
766 |
767 | hit the first visible tag button
768 | """
769 | button = [button for _, button in self.tagButtons
770 | if button.isVisible()][0]
771 | button.click()
772 |
773 |
774 | class Shelves(CustomFrame):
775 | """
776 | Custom display of the notebooks and folder
777 |
778 | """
779 | # Fired when a change is made, so that the Editing panel can also adapt
780 | refreshSignal = QtCore.Signal()
781 | # Fired when a notebook is clicked, to navigate to the editor.
782 | # TODO also define as a shift+click to directly open the previewer
783 | switchTabSignal = QtCore.Signal(str, str)
784 | previewSignal = QtCore.Signal(str)
785 |
786 | def initUI(self):
787 | """Create the physical shelves"""
788 | self.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Sunken)
789 |
790 | self.path = os.path.dirname(__file__)
791 | self.buttons = []
792 |
793 | # update state of UpAction when shelves get refreshed
794 | self.refreshSignal.connect(self.updateUpAction)
795 |
796 | # Store the number of objects per line, for faster redrawing on
797 | # resizing. Initially set to zero, it will, the first time, be set by
798 | # the method createLines, and then be compared to.
799 | self.objectsPerLine = 0
800 | # Left hand side: Vertical layout for the notebooks and folders
801 | scrollArea = VerticalScrollArea(self)
802 |
803 | # Need to create a dummy Widget, because QScrollArea can not accept a
804 | # layout, only a Widget
805 | dummy = QtGui.QWidget()
806 |
807 | vbox = QtGui.QVBoxLayout()
808 | grid = self.createLines()
809 |
810 | vbox.addLayout(grid)
811 | vbox.addStretch(1)
812 | dummy.setLayout(vbox)
813 | scrollArea.setWidget(dummy)
814 |
815 | self.layout().addWidget(scrollArea)
816 |
817 | def refresh(self):
818 | # Redraw the graphical interface.
819 | self.clearUI()
820 | self.initUI()
821 |
822 | # Broadcast a refreshSignal order
823 | self.refreshSignal.emit()
824 |
825 | def createNotebook(self):
826 | self.popup = NewNotebook(self)
827 | ok = self.popup.exec_()
828 | if ok:
829 | desired_name = self.info.notebooks[-1]
830 | self.log.info(desired_name+' is the desired name')
831 | file_name = desired_name
832 | # Create a file, containing only the title
833 | with io.open(os.path.join(self.info.level, file_name),
834 | 'w', encoding='utf-8') as notebook:
835 | clean_name = os.path.splitext(desired_name)[0]
836 | notebook.write(clean_name.capitalize()+'\n')
837 | notebook.write(''.join(['=' for _ in clean_name]))
838 | notebook.write('\n\n')
839 | # Refresh both the library and Editing tab.
840 | self.refresh()
841 |
842 | def createFolder(self):
843 | self.popup = NewFolder(self)
844 | ok = self.popup.exec_()
845 | if ok:
846 | desired_name = self.info.folders[-1]
847 | self.log.info(desired_name+' is the desired name')
848 | folder_name = desired_name
849 | # Create the folder
850 | try:
851 | os.mkdir(os.path.join(self.info.level, folder_name))
852 | except OSError:
853 | # If it already exists, continue
854 | pass
855 | # Change the level to the newly created folder, and send a refresh
856 | # TODO display a warning that an empty folder will be discared if
857 | # browsed out.
858 | folder_path = os.path.join(self.info.root, folder_name)
859 | self.info.notebooks, self.info.folders = search_folder_recursively(
860 | self.log, folder_path, self.info.display_empty)
861 | # Update the current level as the folder_path, and refresh the
862 | # content of the window
863 | self.info.level = folder_path
864 | self.refresh()
865 |
866 | def toggleDisplayEmpty(self):
867 | self.info.display_empty = not self.info.display_empty
868 | # Read again the current folder
869 | self.info.notebooks, self.info.folders = search_folder_recursively(
870 | self.log, self.info.level, self.info.display_empty)
871 | # save settings
872 | self.settings = QtCore.QSettings("audren", "NoteOrganiser")
873 | self.settings.setValue("display_empty", self.info.display_empty)
874 | self.refresh()
875 |
876 | @QtCore.Slot(str)
877 | def removeNotebook(self, notebook):
878 | """Remove the notebook"""
879 | self.log.info(
880 | 'deleting %s from the shelves' % notebook)
881 | path = os.path.join(self.info.level, notebook+EXTENSION)
882 |
883 | # Assert that the file is empty, or ask for confirmation
884 | if os.stat(path).st_size != 0:
885 | self.reply = QtGui.QMessageBox.question(
886 | self, 'Message',
887 | "Are you sure you want to delete %s?" % notebook,
888 | QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
889 | QtGui.QMessageBox.No)
890 | else:
891 | self.reply = QtGui.QMessageBox.Yes
892 |
893 | if self.reply == QtGui.QMessageBox.Yes:
894 | os.remove(path)
895 | # Delete the reference to the notebook
896 | index = self.info.notebooks.index(notebook+EXTENSION)
897 | self.info.notebooks.pop(index)
898 |
899 | # Refresh the display
900 | self.refresh()
901 |
902 | else:
903 | self.log.info("Aborting")
904 |
905 | @QtCore.Slot(str)
906 | def removeFolder(self, folder):
907 | """Remove the folder, with confirmation if non-empty"""
908 | self.log.info(
909 | 'deleting folder %s from the shelves' % folder)
910 | path = os.path.join(self.info.level, folder)
911 |
912 | # Assert that the folder is empty, or ask for confirmation
913 | if not all(os.path.isdir(e) and e[0] == '.' for e in os.listdir(path)):
914 | self.reply = QtGui.QMessageBox.question(
915 | self, 'Message',
916 | "%s still contains notebooks, " % folder +
917 | "are you sure you want to delete it?",
918 | QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
919 | QtGui.QMessageBox.No)
920 | else:
921 | self.reply = QtGui.QMessageBox.Yes
922 |
923 | if self.reply == QtGui.QMessageBox.Yes:
924 | shutil.rmtree(path, ignore_errors=True)
925 | # Delete the reference to the notebook
926 | index = self.info.folders.index(path)
927 | self.info.folders.pop(index)
928 |
929 | # Refresh the display
930 | self.refresh()
931 |
932 | else:
933 | self.log.info("Aborting")
934 |
935 | def notebookClicked(self):
936 | sender = self.sender()
937 | self.log.info('notebook '+sender.label+' button cliked')
938 | # Emit a signal asking for changing the tab
939 | self.switchTabSignal.emit('editing', sender.label)
940 |
941 | def folderClicked(self):
942 | sender = self.sender()
943 | self.log.info('folder '+sender.label+' button cliked')
944 | folder_path = os.path.join(self.info.root, sender.label)
945 | self.info.notebooks, self.info.folders = search_folder_recursively(
946 | self.log, folder_path, self.info.display_empty)
947 | # Update the current level as the folder_path, and refresh the content
948 | # of the window
949 | self.info.level = folder_path
950 | self.refresh()
951 |
952 | def upFolder(self):
953 | folder_path = os.path.dirname(self.info.level)
954 | self.info.notebooks, self.info.folders = search_folder_recursively(
955 | self.log, folder_path, self.info.display_empty)
956 | # Update the current level as the folder_path, and refresh the content
957 | # of the window
958 | self.info.level = folder_path
959 | self.refresh()
960 |
961 | def createLines(self):
962 | # Defining the icon size used
963 | self.size = 128
964 |
965 | # Create the lines array
966 | flow = FlowLayout()
967 | for notebook in self.info.notebooks:
968 | # distinguish between a notebook and a folder, stored as a tuple.
969 | # When encountering a folder, simply put a different image for the
970 | # moment.
971 | button = PicButton(
972 | QtGui.QPixmap(
973 | os.path.join(self.path, 'assets',
974 | 'notebook-%i.png' % self.size)),
975 | os.path.splitext(notebook)[0], 'notebook', self)
976 | button.setMinimumSize(self.size, self.size)
977 | button.setMaximumSize(self.size, self.size)
978 | button.clicked.connect(self.notebookClicked)
979 | button.deleteNotebookSignal.connect(self.removeNotebook)
980 | button.previewSignal.connect(self.previewNotebook)
981 | self.buttons.append(button)
982 | flow.addWidget(button)
983 |
984 | for folder in self.info.folders:
985 | button = PicButton(
986 | QtGui.QPixmap(
987 | os.path.join(self.path, 'assets',
988 | 'folder-%i.png' % self.size)),
989 | os.path.basename(folder), 'folder', self)
990 | button.setMinimumSize(self.size, self.size)
991 | button.setMaximumSize(self.size, self.size)
992 | button.clicked.connect(self.folderClicked)
993 | button.deleteFolderSignal.connect(self.removeFolder)
994 | self.buttons.append(button)
995 | flow.addWidget(button)
996 |
997 | self.flow = flow
998 | return flow
999 |
1000 | @QtCore.Slot(str)
1001 | def previewNotebook(self, notebook):
1002 | """emit signal to preview the current notebook"""
1003 | self.log.info("preview called for notebook %s" % notebook)
1004 | path = os.path.join(self.info.level, notebook+EXTENSION)
1005 | self.previewSignal.emit(path)
1006 |
1007 | def updateUpAction(self):
1008 | """
1009 | update the state of the toolbar action 'Up'
1010 |
1011 | active if not in root
1012 | """
1013 | self.parent.upAction.setDisabled(self.info.level == self.info.root)
1014 |
1015 |
1016 | class TextEditor(CustomFrame):
1017 | """Custom text editor"""
1018 | defaultFontSize = 14
1019 |
1020 | def initUI(self):
1021 | """top menu bar and the text area"""
1022 | # Text
1023 | self.text = CustomTextEdit(self)
1024 | self.text.setTabChangesFocus(True)
1025 |
1026 | # Font
1027 | self.font = QtGui.QFont()
1028 | self.font.setFamily("Inconsolata")
1029 | self.font.setStyleHint(QtGui.QFont.Monospace)
1030 | self.font.setFixedPitch(True)
1031 | self.font.setPointSize(self.defaultFontSize)
1032 |
1033 | self.text.setFont(self.font)
1034 |
1035 | self.highlighter = ModifiedMarkdownHighlighter(self.text.document())
1036 |
1037 | # watch notebooks on the filesystem for changes
1038 | self.fileSystemWatcher = QtCore.QFileSystemWatcher(self)
1039 |
1040 | self.layout().addWidget(self.text)
1041 |
1042 | def setSource(self, source):
1043 | self.log.info("Reading %s" % source)
1044 | self.source = source
1045 | self.loadText()
1046 | self.setupAutoRefresh(source)
1047 |
1048 | def loadText(self):
1049 | if self.source:
1050 | # Store the last cursor position
1051 | oldCursor = self.text.textCursor()
1052 | text = io.open(self.source, 'r', encoding='utf-8',
1053 | errors='replace').read()
1054 | self.text.setText(text)
1055 | self.text.setTextCursor(oldCursor)
1056 | self.text.ensureCursorVisible()
1057 | self.text.document().setModified(False)
1058 |
1059 | def saveText(self):
1060 | self.log.info("Writing modifications to %s" % self.source)
1061 | text = self.text.toPlainText()
1062 | with io.open(self.source, 'w', encoding='utf-8') as file_handle:
1063 | file_handle.write(text)
1064 |
1065 | def appendText(self, text):
1066 | self.text.append('\n'+text)
1067 | self.saveText()
1068 |
1069 | def insertText(self, text):
1070 | self.text.insertPlainText(text)
1071 |
1072 | def zoomIn(self):
1073 | size = self.font.pointSize()
1074 | self.font.setPointSize(size+1)
1075 | self.text.setFont(self.font)
1076 |
1077 | def zoomOut(self):
1078 | size = self.font.pointSize()
1079 | self.font.setPointSize(size-1)
1080 | self.text.setFont(self.font)
1081 |
1082 | def resetSize(self):
1083 | self.font.setPointSize(self.defaultFontSize)
1084 | self.text.setFont(self.font)
1085 |
1086 | def setupAutoRefresh(self, source):
1087 | """add current file to QFileSystemWatcher and refresh when needed"""
1088 | self.fileSystemWatcher.addPath(source)
1089 | self.fileSystemWatcher.fileChanged.connect(
1090 | self.autoRefresh)
1091 | self.log.info("added file %s to FileSystemWatcher" % source)
1092 |
1093 | @QtCore.Slot(str)
1094 | def autoRefresh(self, path=''):
1095 | """refresh editor when needed"""
1096 | # only refresh if wanted and the user didn't modify the text in the
1097 | # internal editor
1098 | if self.info.refreshEditor:
1099 | if not self.text.document().isModified():
1100 | # wait some time for the change to finish
1101 | time.sleep(0.1)
1102 | self.loadText()
1103 | self.fileSystemWatcher.removePath(path)
1104 | self.fileSystemWatcher.addPath(path)
1105 | self.log.info(
1106 | 'editor source reloaded because the file changed')
1107 | else:
1108 | self.log.info(
1109 | "reload of editor source skipped because it's modified")
1110 |
1111 |
1112 | class CustomTextEdit(QtGui.QTextEdit):
1113 |
1114 | def toPlainText(self):
1115 | text = QtGui.QTextEdit.toPlainText(self)
1116 | if isinstance(text, bytes):
1117 | text = str(text)
1118 | return text
1119 |
--------------------------------------------------------------------------------