├── VERSION ├── noteorganiser ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_main_window.py │ ├── test_utils.py │ ├── custom_fixtures.py │ ├── test_widgets.py │ ├── test_text_processing.py │ └── test_frames.py ├── constants.py ├── assets │ ├── folder-128.png │ ├── moleskine-128.png │ ├── notebook-128.png │ ├── laying_notebook-128.png │ ├── laying_notebooks-128.png │ ├── laying_2_notebooks-128.png │ ├── laying_3_notebooks-128.png │ ├── laying_bound_notebook-128.png │ ├── laying_bound_2_notebooks-128.png │ ├── laying_bound_3_notebooks-128.png │ ├── style │ │ ├── fonts │ │ │ └── inconsolata │ │ │ │ ├── inconsolata-webfont.ttf │ │ │ │ ├── stylesheet.css │ │ │ │ ├── generator_config.txt │ │ │ │ ├── specimen_files │ │ │ │ ├── easytabs.js │ │ │ │ ├── grid_12-825-55-15.css │ │ │ │ └── specimen_stylesheet.css │ │ │ │ └── inconsolata-demo.html │ │ ├── bootstrap-blog.html │ │ └── default.css │ └── icon_logic.md ├── logger.py ├── utils.py ├── syntax.py ├── configuration.py ├── widgets.py ├── text_processing.py ├── popups.py ├── NoteOrganiser.py └── frames.py ├── .landscape.yaml ├── .gitignore ├── Makefile ├── .travis.yml ├── LICENSE.txt ├── setup.py ├── example └── example.md ├── roadmap.md └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | -------------------------------------------------------------------------------- /noteorganiser/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /noteorganiser/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /noteorganiser/constants.py: -------------------------------------------------------------------------------- 1 | EXTENSION = '.md' 2 | -------------------------------------------------------------------------------- /noteorganiser/assets/folder-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/folder-128.png -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | doc-warnings: true 2 | test-warnings: false 3 | strictness: medium 4 | max-line-length: 80 5 | autodetect: true 6 | -------------------------------------------------------------------------------- /noteorganiser/assets/moleskine-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/moleskine-128.png -------------------------------------------------------------------------------- /noteorganiser/assets/notebook-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/notebook-128.png -------------------------------------------------------------------------------- /noteorganiser/assets/laying_notebook-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/laying_notebook-128.png -------------------------------------------------------------------------------- /noteorganiser/assets/laying_notebooks-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/laying_notebooks-128.png -------------------------------------------------------------------------------- /noteorganiser/assets/laying_2_notebooks-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/laying_2_notebooks-128.png -------------------------------------------------------------------------------- /noteorganiser/assets/laying_3_notebooks-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/laying_3_notebooks-128.png -------------------------------------------------------------------------------- /noteorganiser/assets/laying_bound_notebook-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/laying_bound_notebook-128.png -------------------------------------------------------------------------------- /noteorganiser/assets/laying_bound_2_notebooks-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/laying_bound_2_notebooks-128.png -------------------------------------------------------------------------------- /noteorganiser/assets/laying_bound_3_notebooks-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/laying_bound_3_notebooks-128.png -------------------------------------------------------------------------------- /noteorganiser/assets/style/fonts/inconsolata/inconsolata-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baudren/NoteOrganiser/HEAD/noteorganiser/assets/style/fonts/inconsolata/inconsolata-webfont.ttf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | dist/ 4 | log 5 | files.txt 6 | *egg-info/ 7 | .coverage 8 | 9 | # coverage reports from py.test 10 | htmlcov/ 11 | 12 | # Idea hidden folder 13 | .idea/ 14 | -------------------------------------------------------------------------------- /noteorganiser/assets/style/fonts/inconsolata/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* Generated by Font Squirrel (http://www.fontsquirrel.com) on October 27, 2014 */ 2 | @font-face { 3 | font-family: 'inconsolatamedium'; 4 | src: url('../fonts/inconsolata/inconsolata-webfont.ttf') format('truetype'); 5 | } 6 | -------------------------------------------------------------------------------- /noteorganiser/assets/icon_logic.md: -------------------------------------------------------------------------------- 1 | 128x128: original size: 2 | 3 | - the "folder" icons should cover y between 0 and 100, not below (for writing) 4 | colors: 5 | * 13: cover 6 | * 12 for antialiasing 7 | * 8: writing zone, pages 8 | * 10: border of writing zone, border of page 9 | * 0 for the elastic band, and lines, writing 10 | * 15 for the binder 11 | * 18 antialiasing 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | python -m noteorganiser/NoteOrganiser 3 | 4 | 3: 5 | python3 noteorganiser/NoteOrganiser.py 6 | 7 | test-2: 8 | py.test-2.7 --cov noteorganiser noteorganiser/ -v --doctest-modules 9 | 10 | test-3: 11 | py.test-3.4 --cov noteorganiser noteorganiser/ -v --doctest-modules --cov-report=html 12 | 13 | test: test-2 test-3 14 | 15 | test-single: 16 | py.test --cov noteorganiser noteorganiser/ -v --doctest-modules --cov-report=html 17 | -------------------------------------------------------------------------------- /noteorganiser/assets/style/fonts/inconsolata/generator_config.txt: -------------------------------------------------------------------------------- 1 | # Font Squirrel Font-face Generator Configuration File 2 | # Upload this file to the generator to recreate the settings 3 | # you used to create these fonts. 4 | 5 | {"mode":"optimal","formats":["ttf","woff","woff2","eotz"],"tt_instructor":"default","fix_vertical_metrics":"Y","fix_gasp":"xy","add_spaces":"Y","add_hyphens":"Y","fallback":"none","fallback_custom":"100","options_subset":"basic","subset_custom":"","subset_custom_range":"","subset_ot_features_list":"","css_stylesheet":"stylesheet.css","filename_suffix":"-webfont","emsquare":"2048","spacing_adjustment":"0"} -------------------------------------------------------------------------------- /noteorganiser/tests/test_main_window.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PySide import QtGui 3 | from PySide import QtCore 4 | # Frame to test 5 | import pytest 6 | from ..NoteOrganiser import NoteOrganiser 7 | 8 | 9 | def test_initialisation(qtbot, mocker): 10 | # Specifying a different folder for storing everything (for testing 11 | # purposes) 12 | home = os.path.expanduser("~") 13 | main = os.path.join(home, '.noteorganiser') 14 | mocker.patch.object( 15 | QtGui.QFileDialog, 'getExistingDirectory', 16 | return_value=main) 17 | note = NoteOrganiser() 18 | # Creating a NoteOrganiser and adding it to the bot 19 | qtbot.addWidget(note) 20 | 21 | # TODO: Setting a command-line editor via the popup and controlling the 22 | # result 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | sudo: false 6 | addons: 7 | apt: 8 | packages: 9 | - pandoc 10 | before_install: 11 | - "export DISPLAY=:99.0" 12 | - "sh -e /etc/init.d/xvfb start" 13 | install: 14 | - pip install PySide --no-index --find-links https://parkin.github.io/python-wheelhouse/; 15 | # Travis CI servers use virtualenvs, so we need to finish the install by the following 16 | - python ~/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/pyside_postinstall.py -install 17 | - pip install qtpy 18 | - pip install qtawesome 19 | - pip install coveralls 20 | - pip install pypandoc 21 | - pip install pandocfilters 22 | - pip install pytest 23 | - pip install 'pytest-qt>=1.2.1' 24 | - pip install pytest-cov 25 | - pip install pytest-mock 26 | - python setup.py install 27 | script: make test-single 28 | after_success: 29 | coveralls 30 | -------------------------------------------------------------------------------- /noteorganiser/assets/style/fonts/inconsolata/specimen_files/easytabs.js: -------------------------------------------------------------------------------- 1 | (function($){$.fn.easyTabs=function(option){var param=jQuery.extend({fadeSpeed:"fast",defaultContent:1,activeClass:'active'},option);$(this).each(function(){var thisId="#"+this.id;if(param.defaultContent==''){param.defaultContent=1;} 2 | if(typeof param.defaultContent=="number") 3 | {var defaultTab=$(thisId+" .tabs li:eq("+(param.defaultContent-1)+") a").attr('href').substr(1);}else{var defaultTab=param.defaultContent;} 4 | $(thisId+" .tabs li a").each(function(){var tabToHide=$(this).attr('href').substr(1);$("#"+tabToHide).addClass('easytabs-tab-content');});hideAll();changeContent(defaultTab);function hideAll(){$(thisId+" .easytabs-tab-content").hide();} 5 | function changeContent(tabId){hideAll();$(thisId+" .tabs li").removeClass(param.activeClass);$(thisId+" .tabs li a[href=#"+tabId+"]").closest('li').addClass(param.activeClass);if(param.fadeSpeed!="none") 6 | {$(thisId+" #"+tabId).fadeIn(param.fadeSpeed);}else{$(thisId+" #"+tabId).show();}} 7 | $(thisId+" .tabs li").click(function(){var tabId=$(this).find('a').attr('href').substr(1);changeContent(tabId);return false;});});}})(jQuery); -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Benjamin Audren 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /noteorganiser/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """tests for utilities""" 2 | 3 | import os 4 | import shutil 5 | import datetime 6 | from PySide import QtGui 7 | from PySide import QtCore 8 | 9 | #utils to test 10 | from ..utils import fuzzySearch 11 | from .custom_fixtures import parent 12 | 13 | 14 | def test_fuzzySearch(): 15 | ### these should return True 16 | 17 | #starts with the searchstring 18 | assert fuzzySearch('g', 'git got gut') 19 | #starts with the (longer) searchstring 20 | assert fuzzySearch('git', 'git got gut') 21 | #searchstring not at the start 22 | assert fuzzySearch('got', 'git got gut') 23 | #multiple substrings (separated by a space) found somewhere in the string 24 | assert fuzzySearch('gi go', 'git got gut') 25 | #empty string 26 | assert fuzzySearch('', 'git got gut') 27 | #strange whitespace 28 | assert fuzzySearch('gi go', 'git got gut') 29 | assert fuzzySearch('gi go', 'git got gut') 30 | 31 | ### these should return False 32 | 33 | #searchstring not found 34 | assert not fuzzySearch('bot', 'git got gut') 35 | #searchstring not found 36 | assert not fuzzySearch('gran', 'this is a great neat thing') 37 | -------------------------------------------------------------------------------- /noteorganiser/logger.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import logging 3 | 4 | 5 | def create_logger(level='DEBUG', handler_type='stream', path=''): 6 | """Defines a logger with optional level""" 7 | 8 | # Recover the associate value to the specified level 9 | level_value = getattr(logging, level, 0) 10 | 11 | logger = logging.getLogger('simple_example') 12 | logger.name = "_name_" 13 | logger.setLevel(level_value) 14 | 15 | # create the handler depengin on the desired type 16 | if handler_type == 'stream': 17 | handler = logging.StreamHandler() 18 | elif handler_type == 'file': 19 | handler = logging.FileHandler(path, mode='w') 20 | else: 21 | handler = logging.NullHandler() 22 | 23 | handler.setLevel(level_value) 24 | 25 | # create formatter 26 | formatter = logging.Formatter( 27 | "%(module) 17s: L%(lineno) 4s %(funcName) 25s" 28 | " | %(levelname) -10s --> %(message)s") 29 | #"%(asctime)s %(module)s: L%(lineno) 4s %(funcName) 25s" 30 | #" | %(levelname) -10s --> %(message)s") 31 | 32 | # add formatter to the console handler 33 | handler.setFormatter(formatter) 34 | 35 | # add the console handler to logger 36 | logger.addHandler(handler) 37 | 38 | return logger 39 | -------------------------------------------------------------------------------- /noteorganiser/assets/style/bootstrap-blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | $for(author-meta)$ 11 | 12 | $endfor$ 13 | $if(date-meta)$ 14 | 15 | $endif$ 16 | $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ 17 | $if(highlighting-css)$ 18 | 21 | $endif$ 22 | $for(css)$ 23 | 24 | $endfor$ 25 | $if(math)$ 26 | $math$ 27 | $endif$ 28 | $for(header-includes)$ 29 | $header-includes$ 30 | $endfor$ 31 | 32 | 33 | $for(include-before)$ 34 | $include-before$ 35 | $endfor$ 36 |
37 | $if(toc)$ 38 |
39 | $toc$ 40 |
41 | $endif$ 42 | $body$ 43 |
44 | $for(include-after)$ 45 | $include-after$ 46 | $endfor$ 47 | 48 | 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from setuptools import find_packages 3 | 4 | import os 5 | 6 | 7 | # Find all packages 8 | PACKAGES = find_packages() 9 | 10 | # Platform independent recovery of the home directory. It is always put as 11 | # a hidden folder, '.noteorganiser', in the unix tradition. 12 | MAIN = os.path.join(os.path.expanduser("~"), '.noteorganiser') 13 | 14 | # Recover the data files, and place them 15 | ASSET_FOLDER = os.path.join('noteorganiser', 'assets') 16 | STYLE_FOLDER = os.path.join(ASSET_FOLDER, 'style') 17 | ASSETS = [('', ['VERSION']), 18 | (MAIN, 19 | [os.path.join('example', 'example.md')]), 20 | (ASSET_FOLDER, 21 | [os.path.join(ASSET_FOLDER, 'notebook-128.png'), 22 | os.path.join(ASSET_FOLDER, 'folder-128.png')]), 23 | (STYLE_FOLDER, 24 | [os.path.join(STYLE_FOLDER, 'default.css'), 25 | os.path.join(STYLE_FOLDER, 'bootstrap.css'), 26 | os.path.join(STYLE_FOLDER, 'bootstrap-blog.html')]), ] 27 | 28 | setup(name='NoteOrganiser', 29 | version=open('VERSION').read().strip(), 30 | description='Note Organiser for Scientists', 31 | long_description=open('README.md').read(), 32 | author='Benjamin Audren', 33 | author_email='benjamin.audren@gmail.com', 34 | license='MIT', 35 | url='https://github.com/baudren/NoteOrganiser', 36 | packages=PACKAGES, 37 | scripts=['noteorganiser/NoteOrganiser.py'], 38 | install_requires=['pypandoc', 'six', 'PySide>=1.2.2', 'qtawesome', 39 | 'qtpy', 'pygments'], 40 | data_files=ASSETS, 41 | ) 42 | -------------------------------------------------------------------------------- /example/example.md: -------------------------------------------------------------------------------- 1 | Pyside 2 | ====== 3 | 4 | 5 | 6 | 7 | Deleting all items in a layout 8 | ------------------------------ 9 | # layout, widget, clear 10 | 11 | *05/09/2014* 12 | 13 | When refreshing a widget/frame/layout, and want to erase all previous things on 14 | the frame, it is good to have a high level layout, set initially, to which you 15 | add things. You will then be deleting layouts and widgets out of this global 16 | layout, with the following two methods: 17 | 18 | ~~~ python 19 | def clearUI(self): 20 | while self.layout().count(): 21 | item = self.layout().takeAt(0) 22 | if isinstance(item, QtGui.QLayout): 23 | self.clearLayout(item) 24 | item.deleteLater() 25 | else: 26 | try: 27 | widget = item.widget() 28 | if widget is not None: 29 | widget.deleteLater() 30 | except AttributeError: 31 | pass 32 | 33 | def clearLayout(self, layout): 34 | if layout is not None: 35 | while layout.count(): 36 | item = layout.takeAt(0) 37 | widget = item.widget() 38 | if widget is not None: 39 | widget.deleteLater() 40 | else: 41 | self.clearLayout(item.layout()) 42 | ~~~ 43 | 44 | There are perharps more efficient ways to go, but this one works well. To 45 | refresh a page, simply call `self.clearUI(); self.initUI()`. 46 | 47 | 48 | Disabling buttons 49 | ----------------- 50 | # button, disable, scroll 51 | 52 | *08/09/2014* 53 | 54 | When disabling a QPushButton, with `button.setDisabled(True)`, any scrolling 55 | behaviour associated with it will not work any more. You should use 56 | `button.setFlat(True)` to obtain the desired behaviour. 57 | -------------------------------------------------------------------------------- /noteorganiser/tests/custom_fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import datetime 4 | import pytest 5 | from PySide import QtGui 6 | 7 | from ..logger import create_logger 8 | from ..configuration import search_folder_recursively 9 | from .. import configuration as conf 10 | 11 | 12 | @pytest.fixture 13 | def parent(request, qtbot): 14 | date = str(datetime.date.today()) 15 | home = os.path.join(os.path.expanduser("~"), '.noteorganiser') 16 | # Create the temp folder 17 | temp_folder_path = os.path.join( 18 | home, '.test_%s' % date) 19 | if not os.path.isdir(temp_folder_path): 20 | os.mkdir(temp_folder_path) 21 | # Copy there twice the example.md file, create a subfolder, and put again 22 | # the same example.md in the subfolder 23 | shutil.copy( 24 | os.path.join(os.path.os.getcwd(), 'example', 'example.md'), 25 | temp_folder_path) 26 | shutil.copyfile( 27 | os.path.join(temp_folder_path, 'example.md'), 28 | os.path.join(temp_folder_path, 'second.md')) 29 | subfolder = os.path.join(temp_folder_path, 'toto') 30 | os.mkdir(subfolder) 31 | shutil.copy( 32 | os.path.join(os.path.os.getcwd(), 'example', 'example.md'), 33 | subfolder) 34 | # Create a parent window, containing an information instance, and a 35 | # logger 36 | parent = QtGui.QMainWindow() 37 | qtbot.addWidget(parent) 38 | log = create_logger('CRITICAL', 'stream') 39 | # Create an info instance 40 | # Search the folder recursively 41 | notebooks, folders = search_folder_recursively(log, temp_folder_path) 42 | info = conf.Information(log, temp_folder_path, notebooks, folders) 43 | parent.info = info 44 | parent.log = log 45 | 46 | def fin(): 47 | """Tear down parent class""" 48 | shutil.rmtree(temp_folder_path) 49 | parent.destroy() 50 | request.addfinalizer(fin) 51 | 52 | return parent 53 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | Backlog 2 | ======= 3 | 4 | # General 5 | - [ ] Document more. 6 | - [ ] Improve keyboard shortcuts (ctrl instead of alt, etc). 7 | - [ ] Ensure proper key navigation at all times (sometimes ctrl+tab does not 8 | switch between tabs) 9 | - [ ] testing the right-click menu on buttons 10 | - [ ] Allow for a different location of the .noteorganiser folder (on SpiderOak 11 | instead of home directory, for instance) 12 | - [ ] Fix the initial window size (#37) 13 | 14 | # Library 15 | - [ ] Draw vector icons, and use several sizes 16 | - [ ] Have an application icon (#31 and #32) 17 | - [ ] Display all tags in this zone 18 | - [ ] Display the current folder's name somewhere 19 | 20 | # Editing 21 | - [ ] List of existing tags when entering a new field 22 | - [ ] Keep cursor position on reload 23 | - [ ] Have a basic search engine (#39) 24 | - [ ] The NewEntry form should not accept "ESC" as a cancel option if there is 25 | text in the TextEdit block. 26 | 27 | # Preview 28 | - [ ] better overall css style (#40) 29 | - [ ] Have a table of contents (#16) 30 | - [ ] Button Refresh in Preview (not that external editor works) (#34) 31 | - [ ] change the graphics of setFlat to match the disabled look, without the 32 | drawback of preventing scrolling. 33 | - [ ] have a "global" page, storing all notebooks, filter added with the 34 | notebooks' name as a tag 35 | - [ ] have several options for tag sorting: 36 | - [ ] importance (which also should use alphabetical for equally important tags) 37 | - [ ] alphabetical 38 | - [ ] cloud (*a la* Wiki) 39 | 40 | 41 | Future work 42 | =========== 43 | 44 | - [ ] have a preview of the entry while typing it 45 | - [ ] rethink the user experience: library panel? browsing? 46 | - [ ] colored icons and tags, with colors chosen by the user 47 | - [ ] support well multi-screen 48 | - [ ] use only one webpage, and stop displaying members instead of generating all 49 | these webpages! Using an underlying sort of table system, which you could 50 | turn on/off (javascript?) 51 | 52 | # Library 53 | - [ ] special graphic for shelves, maybe wood? 54 | -------------------------------------------------------------------------------- /noteorganiser/assets/style/default.css: -------------------------------------------------------------------------------- 1 | 142 | -------------------------------------------------------------------------------- /noteorganiser/tests/test_widgets.py: -------------------------------------------------------------------------------- 1 | """tests for custom widgets""" 2 | 3 | from PySide import QtGui 4 | from PySide import QtCore 5 | 6 | #widgets to test 7 | from ..widgets import LineEditWithClearButton 8 | from ..widgets import TagCompletion 9 | from ..utils import MultiCompleter 10 | from .custom_fixtures import parent 11 | 12 | 13 | def test_LineEditWithClearButton(qtbot, parent): 14 | lineEdit = LineEditWithClearButton() 15 | qtbot.addWidget(lineEdit) 16 | 17 | assert hasattr(lineEdit, 'clearButton') 18 | 19 | assert not len(lineEdit.text()) 20 | assert not lineEdit.clearButton.isVisibleTo(lineEdit) 21 | qtbot.keyClicks(lineEdit, 'titi') 22 | assert len(lineEdit.text()) 23 | assert lineEdit.clearButton.isVisibleTo(lineEdit) 24 | qtbot.mouseClick(lineEdit.clearButton, QtCore.Qt.LeftButton) 25 | assert not len(lineEdit.text()) 26 | assert not lineEdit.clearButton.isVisibleTo(lineEdit) 27 | 28 | 29 | def test_TagCompletion(qtbot, parent): 30 | tags = ['toto', 'tata'] 31 | tagCompletion = TagCompletion(tags) 32 | qtbot.addWidget(tagCompletion) 33 | 34 | # check that our Completer is our own MultiCompleter 35 | assert isinstance(tagCompletion.completer, MultiCompleter) 36 | 37 | # check filtering 38 | qtbot.keyClicks(tagCompletion, 't') 39 | assert tagCompletion.completer.completionModel().rowCount() == 2 40 | qtbot.keyClicks(tagCompletion, 'a') 41 | assert tagCompletion.completer.completionModel().rowCount() == 1 42 | qtbot.keyClicks(tagCompletion, 'x') 43 | assert tagCompletion.completer.completionModel().rowCount() == 0 44 | 45 | tagCompletion.clear() 46 | assert tagCompletion.text() == '' 47 | 48 | # check completion 49 | qtbot.keyClicks(tagCompletion, 't') 50 | qtbot.keyPress(tagCompletion, QtCore.Qt.Key_Enter) 51 | assert tagCompletion.text() == ' tata' 52 | 53 | # check second completion after separator 54 | qtbot.keyClicks(tagCompletion, ', to') 55 | qtbot.keyPress(tagCompletion, QtCore.Qt.Key_Enter) 56 | assert tagCompletion.text() == ' tata, toto' 57 | 58 | # check other separator 59 | qtbot.keyClicks(tagCompletion, '; ta') 60 | qtbot.keyPress(tagCompletion, QtCore.Qt.Key_Enter) 61 | assert tagCompletion.text() == ' tata, toto; tata' 62 | 63 | # check that another press on Return doesn't change the text 64 | qtbot.keyPress(tagCompletion, QtCore.Qt.Key_Enter) 65 | assert tagCompletion.text() == ' tata, toto; tata' 66 | 67 | # check the down button 68 | assert not tagCompletion.completer.popup().isVisible() 69 | qtbot.mouseClick(tagCompletion.downButton, QtCore.Qt.LeftButton) 70 | assert tagCompletion.completer.popup().isVisible() 71 | 72 | # check normalization of separators 73 | assert tagCompletion.getTextWithNormalizedSeparators() == \ 74 | ' tata, toto, tata' 75 | 76 | # check switching of separators 77 | tagCompletion.completer.setSeparators([';', ',']) 78 | assert tagCompletion.completer.separators == [';', ','] 79 | assert tagCompletion.getTextWithNormalizedSeparators() == \ 80 | ' tata; toto; tata' 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Note Organiser for scientists 2 | ============================= 3 | 4 | [![Join the chat at https://gitter.im/baudren/NoteOrganiser](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/baudren/NoteOrganiser?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | [![Build Status](https://travis-ci.org/baudren/NoteOrganiser.png?branch=devel)](https://travis-ci.org/baudren/NoteOrganiser) 7 | [![Coverage Status](https://coveralls.io/repos/baudren/NoteOrganiser/badge.png?branch=devel)](https://coveralls.io/r/baudren/NoteOrganiser?branch=devel) 8 | [![Health](https://landscape.io/github/baudren/NoteOrganiser/devel/landscape.png)](https://landscape.io/github/baudren/NoteOrganiser/devel) 9 | [![Stories in 10 | Ready](https://badge.waffle.io/baudren/noteorganiser.png?label=ready&title=Ready)](http://waffle.io/baudren/noteorganiser) 11 | 12 | Objective 13 | --------- 14 | 15 | Provide scientists, IT professionals but also normal people, with a solid, 16 | lightweight note-taking GUI, with tag browsing and a clear interface. 17 | 18 | Based on a customized markdown syntax, it supports mathematics. The 19 | modification of the syntax, parsed internally, allows to store tags for posts, 20 | for easier filtering when previewing a large document. 21 | 22 | It is aimed to store small notes, remarks on program, life-pro-tips found while 23 | developing/researching, in a safe place, reachable without internet access. 24 | 25 | Eventually, it should also support longer posts (blog-like), and more 26 | specialised types of notes, like ones for experimental work. 27 | 28 | Installation 29 | ------------ 30 | 31 | Note Organiser uses the standard Python distutils tool for installing. Simply 32 | issue: 33 | 34 | python setup.py install --user 35 | 36 | when in the main directory. It requires PySide, and pypandoc, which will be 37 | installed if not present. **Be warned, PySide is a huge install**. Go walk 38 | outside for a bit. 39 | 40 | To get you started, look at the file `example/example.md`. 41 | 42 | Usage 43 | ----- 44 | 45 | Simply run, from anywhere, `NoteOrganiser.py`. Create notebooks, add entries 46 | with the `New Entry` button in the Editing panel, and preview them. On the 47 | `Preview` panel, you can filter entries with tags. 48 | 49 | Markdown 50 | -------- 51 | 52 | For those still not familiar with Markdown, you can check the original code, 53 | developed by John Gruber 54 | [here](http://daringfireball.net/projects/markdown/syntax). It is a markup 55 | language designed to be clear and readable, and easily converted to html for a 56 | better visualisation. 57 | 58 | Note Organiser converts notes taken in markdown with an additional syntax for 59 | tags and date. The conversion is made through the python wrapper 60 | [pypandoc](https://github.com/bebraw/pypandoc) of the 61 | [pandoc](https://github.com/jgm/pandoc) document converter. The format will be 62 | recognised automatically, and supports most extensions of markdown. A useful 63 | cheat-sheet to consult is available [on this Github 64 | wiki](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). 65 | 66 | 67 | Current Features 68 | ---------------- 69 | 70 | - Markdown syntax for a clear source file, readable without the software. 71 | - Tag system for entries for classification 72 | - Html previewing of the note file, with tag sorting 73 | 74 | see the roadmap (display in raw format, since Github does not apply the Github 75 | flavored syntax to files) for coming features, and make use of the issues page 76 | to propose improvements. 77 | 78 | License 79 | ------- 80 | 81 | The code is published under the MIT license, please see LICENSE.txt for the 82 | complete notice. 83 | 84 | 85 | Contributing 86 | ------------ 87 | 88 | Contributions are welcome, so please submit a bug-report or a feature request. 89 | Pull-Request are also very appreciated. Please think about running the tests 90 | under both python 2.7 and 3.3 before submitting, though! 91 | 92 | ## Contributors 93 | 94 | - Tobias Maier ([@egolus](https://github.com/egolus)), for his many 95 | contributions, and help in supporting Windows. 96 | -------------------------------------------------------------------------------- /noteorganiser/tests/test_text_processing.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import os 3 | import pytest 4 | from datetime import date 5 | from ..text_processing import * 6 | from .custom_fixtures import parent 7 | from collections import OrderedDict as od 8 | 9 | import six 10 | 11 | def test_full_file(parent): 12 | # Full list of tags, to update 13 | tags = ['layout', 'widget', 'scroll', 'clear', 'button', 'disable'] 14 | 15 | source = os.path.join(parent.info.root, parent.info.notebooks[-1]) 16 | text = open(source).readlines() 17 | # Add an extra blanck line at the beginning of the text 18 | text = [" \t \n"] + text 19 | title, posts = extract_title_and_posts_from_text(text) 20 | # Check that the title is correct 21 | assert title == 'Pyside' 22 | 23 | # Check that there is the right amount of posts 24 | assert len(posts) == 2 25 | 26 | # check that the whole file is well interpreted with no tags in input 27 | markdown, extracted_tags = from_notes_to_markdown(source, []) 28 | assert len(extracted_tags) == 6 29 | keys = [e for e in extracted_tags.keys()] 30 | assert set(keys) == set(tags) 31 | 32 | # check the reduction of tags 33 | markdown, reduced_tags = from_notes_to_markdown(source, ['layout']) 34 | reduced_keys = [e for e in reduced_tags] 35 | assert len(reduced_tags) == 3 36 | assert set(reduced_keys) == set(['layout', 'widget', 'clear']) 37 | 38 | 39 | def test_tag_sorting(): 40 | source = ['toto', 'toto', 'toto', 'tata', 'titi', 'titi', 'titi', 'titi'] 41 | ordered_keys = ['titi', 'toto', 'tata'] 42 | output = sort_tags(source) 43 | # Check the output type 44 | assert isinstance(output, od) 45 | 46 | # Check the order 47 | assert [k for k in output.keys()] == ordered_keys 48 | 49 | 50 | def test_extract_tags_from_post(): 51 | # Check for a good post 52 | post = ['Toto', '-------', ' # non-linear, pk', '*21/12/2012*'] 53 | tags, clean_post = extract_tags_from_post(post) 54 | assert len(tags) == 2 55 | assert tags == ['non-linear', 'pk'] 56 | assert clean_post == ['Toto', '-------', '*21/12/2012*'] 57 | 58 | # Check for empty tags 59 | empty_tag_post = ['Toto', '-------', '# ', '*21/12/2012*'] 60 | with pytest.raises(ValueError): 61 | tags, clean_post = extract_tags_from_post(empty_tag_post) 62 | 63 | 64 | def test_is_valid_post(): 65 | 66 | good = ["Toto", "-------", "# non-linear, pk", "*21/12/2012*"] 67 | assert is_valid_post(good) 68 | 69 | with pytest.raises(ValueError): 70 | is_valid_post(["Toto", "-------", "*21/12/2012*"]) 71 | 72 | with pytest.raises(ValueError): 73 | is_valid_post(["Toto", "-----", "# something", "12/12/042*"]) 74 | 75 | with pytest.raises(ValueError): 76 | is_valid_post(['Toto', '=======', '# something', '*21/12/2012*']) 77 | 78 | with pytest.raises(ValueError): 79 | is_valid_post(['', '-------', '# non-linear, pk', '*21/12/2012*']) 80 | 81 | with pytest.raises(ValueError): 82 | is_valid_post(['Toto', '-------', '*21/12/2012*', 'something']) 83 | 84 | 85 | def test_extract_corpus_from_post(): 86 | post = ["Toto", "-------", "This morning I woke", "", 87 | "up and it was a nice weather"] 88 | answer = extract_corpus_from_post(post) 89 | assert answer == ['This morning I woke', '', 90 | 'up and it was a nice weather'] 91 | 92 | 93 | def test_extract_title_from_post(): 94 | post = ["Toto", "-------", "# non-linear, pk", "*21/12/2012*"] 95 | answer = extract_title_from_post(post) 96 | assert answer == 'Toto' 97 | 98 | 99 | def test_extract_date_from_post(): 100 | post = ["Toto", "-------", "*21/12/2012*", "Something something"] 101 | post_date, clean_post = extract_date_from_post(post) 102 | assert post_date == date(2012, 12, 21) 103 | assert clean_post == ['Toto', '-------', 'Something something'] 104 | with pytest.raises(ValueError): 105 | extract_date_from_post(["Toto", "---------", "meh"]) 106 | 107 | 108 | def test_normalize_post(): 109 | post = ['Toto', 'has a long title', '-------', '# bla', '*08/11/2010*'] 110 | answer = normalize_post(post) 111 | assert answer == ['Toto has a long title', '-------', 112 | '# bla', '*08/11/2010*'] 113 | assert is_valid_post(answer) 114 | 115 | 116 | def test_post_to_markdown(): 117 | no_blank_line = ['Toto', '------', '#tag', '*25/04/2015*', '', 'post'] 118 | normalized = normalize_post(no_blank_line) 119 | text, tags = post_to_markdown(normalized) 120 | assert 'post' in text 121 | -------------------------------------------------------------------------------- /noteorganiser/assets/style/fonts/inconsolata/specimen_files/grid_12-825-55-15.css: -------------------------------------------------------------------------------- 1 | /*Notes about grid: 2 | Columns: 12 3 | Grid Width: 825px 4 | Column Width: 55px 5 | Gutter Width: 15px 6 | -------------------------------*/ 7 | 8 | 9 | 10 | .section {margin-bottom: 18px; 11 | } 12 | .section:after {content: ".";display: block;height: 0;clear: both;visibility: hidden;} 13 | .section {*zoom: 1;} 14 | 15 | .section .firstcolumn, 16 | .section .firstcol {margin-left: 0;} 17 | 18 | 19 | /* Border on left hand side of a column. */ 20 | .border { 21 | padding-left: 7px; 22 | margin-left: 7px; 23 | border-left: 1px solid #eee; 24 | } 25 | 26 | /* Border with more whitespace, spans one column. */ 27 | .colborder { 28 | padding-left: 42px; 29 | margin-left: 42px; 30 | border-left: 1px solid #eee; 31 | } 32 | 33 | 34 | 35 | /* The Grid Classes */ 36 | .grid1, .grid1_2cols, .grid1_3cols, .grid1_4cols, .grid2, .grid2_3cols, .grid2_4cols, .grid3, .grid3_2cols, .grid3_4cols, .grid4, .grid4_3cols, .grid5, .grid5_2cols, .grid5_3cols, .grid5_4cols, .grid6, .grid6_4cols, .grid7, .grid7_2cols, .grid7_3cols, .grid7_4cols, .grid8, .grid8_3cols, .grid9, .grid9_2cols, .grid9_4cols, .grid10, .grid10_3cols, .grid10_4cols, .grid11, .grid11_2cols, .grid11_3cols, .grid11_4cols, .grid12 37 | {margin-left: 15px;float: left;display: inline; overflow: hidden;} 38 | 39 | 40 | .width1, .grid1, .span-1 {width: 55px;} 41 | .width1_2cols,.grid1_2cols {width: 20px;} 42 | .width1_3cols,.grid1_3cols {width: 8px;} 43 | .width1_4cols,.grid1_4cols {width: 2px;} 44 | .input_width1 {width: 49px;} 45 | 46 | .width2, .grid2, .span-2 {width: 125px;} 47 | .width2_3cols,.grid2_3cols {width: 31px;} 48 | .width2_4cols,.grid2_4cols {width: 20px;} 49 | .input_width2 {width: 119px;} 50 | 51 | .width3, .grid3, .span-3 {width: 195px;} 52 | .width3_2cols,.grid3_2cols {width: 90px;} 53 | .width3_4cols,.grid3_4cols {width: 37px;} 54 | .input_width3 {width: 189px;} 55 | 56 | .width4, .grid4, .span-4 {width: 265px;} 57 | .width4_3cols,.grid4_3cols {width: 78px;} 58 | .input_width4 {width: 259px;} 59 | 60 | .width5, .grid5, .span-5 {width: 335px;} 61 | .width5_2cols,.grid5_2cols {width: 160px;} 62 | .width5_3cols,.grid5_3cols {width: 101px;} 63 | .width5_4cols,.grid5_4cols {width: 72px;} 64 | .input_width5 {width: 329px;} 65 | 66 | .width6, .grid6, .span-6 {width: 405px;} 67 | .width6_4cols,.grid6_4cols {width: 90px;} 68 | .input_width6 {width: 399px;} 69 | 70 | .width7, .grid7, .span-7 {width: 475px;} 71 | .width7_2cols,.grid7_2cols {width: 230px;} 72 | .width7_3cols,.grid7_3cols {width: 148px;} 73 | .width7_4cols,.grid7_4cols {width: 107px;} 74 | .input_width7 {width: 469px;} 75 | 76 | .width8, .grid8, .span-8 {width: 545px;} 77 | .width8_3cols,.grid8_3cols {width: 171px;} 78 | .input_width8 {width: 539px;} 79 | 80 | .width9, .grid9, .span-9 {width: 615px;} 81 | .width9_2cols,.grid9_2cols {width: 300px;} 82 | .width9_4cols,.grid9_4cols {width: 142px;} 83 | .input_width9 {width: 609px;} 84 | 85 | .width10, .grid10, .span-10 {width: 685px;} 86 | .width10_3cols,.grid10_3cols {width: 218px;} 87 | .width10_4cols,.grid10_4cols {width: 160px;} 88 | .input_width10 {width: 679px;} 89 | 90 | .width11, .grid11, .span-11 {width: 755px;} 91 | .width11_2cols,.grid11_2cols {width: 370px;} 92 | .width11_3cols,.grid11_3cols {width: 241px;} 93 | .width11_4cols,.grid11_4cols {width: 177px;} 94 | .input_width11 {width: 749px;} 95 | 96 | .width12, .grid12, .span-12 {width: 825px;} 97 | .input_width12 {width: 819px;} 98 | 99 | /* Subdivided grid spaces */ 100 | .emptycols_left1, .prepend-1 {padding-left: 70px;} 101 | .emptycols_right1, .append-1 {padding-right: 70px;} 102 | .emptycols_left2, .prepend-2 {padding-left: 140px;} 103 | .emptycols_right2, .append-2 {padding-right: 140px;} 104 | .emptycols_left3, .prepend-3 {padding-left: 210px;} 105 | .emptycols_right3, .append-3 {padding-right: 210px;} 106 | .emptycols_left4, .prepend-4 {padding-left: 280px;} 107 | .emptycols_right4, .append-4 {padding-right: 280px;} 108 | .emptycols_left5, .prepend-5 {padding-left: 350px;} 109 | .emptycols_right5, .append-5 {padding-right: 350px;} 110 | .emptycols_left6, .prepend-6 {padding-left: 420px;} 111 | .emptycols_right6, .append-6 {padding-right: 420px;} 112 | .emptycols_left7, .prepend-7 {padding-left: 490px;} 113 | .emptycols_right7, .append-7 {padding-right: 490px;} 114 | .emptycols_left8, .prepend-8 {padding-left: 560px;} 115 | .emptycols_right8, .append-8 {padding-right: 560px;} 116 | .emptycols_left9, .prepend-9 {padding-left: 630px;} 117 | .emptycols_right9, .append-9 {padding-right: 630px;} 118 | .emptycols_left10, .prepend-10 {padding-left: 700px;} 119 | .emptycols_right10, .append-10 {padding-right: 700px;} 120 | .emptycols_left11, .prepend-11 {padding-left: 770px;} 121 | .emptycols_right11, .append-11 {padding-right: 770px;} 122 | .pull-1 {margin-left: -70px;} 123 | .push-1 {margin-right: -70px;margin-left: 18px;float: right;} 124 | .pull-2 {margin-left: -140px;} 125 | .push-2 {margin-right: -140px;margin-left: 18px;float: right;} 126 | .pull-3 {margin-left: -210px;} 127 | .push-3 {margin-right: -210px;margin-left: 18px;float: right;} 128 | .pull-4 {margin-left: -280px;} 129 | .push-4 {margin-right: -280px;margin-left: 18px;float: right;} -------------------------------------------------------------------------------- /noteorganiser/utils.py: -------------------------------------------------------------------------------- 1 | """custom utilities""" 2 | 3 | from __future__ import unicode_literals 4 | 5 | from PySide import QtGui 6 | from PySide import QtCore 7 | 8 | import re 9 | 10 | 11 | def fuzzySearch(searchInput, baseString): 12 | """fuzzy comparison of the strings""" 13 | #normalize strings 14 | searchInput = re.sub(r'\s', r' ', searchInput.lower()) 15 | baseString = re.sub(r'\s', r' ', baseString.lower()) 16 | 17 | #normal substring comparison 18 | if searchInput in baseString: 19 | return True 20 | 21 | # split searchInput and check them separately 22 | if ' ' in searchInput: 23 | if not [subString for subString in searchInput.split(' ') if not 24 | fuzzySearch(subString, baseString)]: 25 | return True 26 | 27 | # no match found 28 | return False 29 | 30 | 31 | class MultiCompleter(QtGui.QCompleter): 32 | """ 33 | Custom completer for multiple items 34 | 35 | This completer can be used in every text-widget 36 | The separator can be set with setSeparator. 37 | The standard separator is ',' 38 | 39 | example to set it for a widget: 40 | tester = QtGui.QLineEdit 41 | tags = ['items', 'to', 'complete'] 42 | completer = MultiCompleter(tags, self) 43 | tester.setCompleter(completer) 44 | """ 45 | # separator for multi-item completion 46 | separators = [',', ';'] 47 | 48 | def pathFromIndex(self, index): 49 | """ 50 | add the completed string to the whole string 51 | 52 | this replaces the substring after the last instance of separator 53 | with the completed string 54 | """ 55 | path = QtGui.QCompleter.pathFromIndex(self, index) 56 | 57 | oldText = str(self.widget().text()) 58 | path = re.sub(r'[^{0}]*$'.format(''.join(self.separators)), 59 | ' ' + path, oldText) 60 | 61 | return path 62 | 63 | def splitPath(self, path): 64 | """ 65 | get the substring after the last instace of separator 66 | 67 | this substring is used for completion 68 | """ 69 | path = re.sub('.*[{0}]\s*|\s*'.format(''.join(self.separators)), 70 | '', path) 71 | return [path] 72 | 73 | def setSeparators(self, separators): 74 | """ 75 | change the separators for multi-item completion 76 | 77 | the standard separators are [',', ';'] 78 | """ 79 | if separators: 80 | self.separators = separators 81 | 82 | 83 | class FlowLayout(QtGui.QLayout): 84 | """ 85 | PySide port of the layouts/flowlayout example from Qt v4.x 86 | The flowlayout automatically rearranges its items to fit horizontally 87 | 88 | this class is taken from the pyside examples: 89 | https://github.com/PySide/Examples/tree/master/examples/layouts 90 | """ 91 | 92 | def __init__(self, parent=None, margin=0, spacing=-1): 93 | 94 | QtGui.QLayout.__init__(self, parent) 95 | 96 | if parent is not None: 97 | self.setMargin(margin) 98 | 99 | self.setSpacing(spacing) 100 | 101 | self.itemList = [] 102 | 103 | def __del__(self): 104 | item = self.takeAt(0) 105 | while item: 106 | item = self.takeAt(0) 107 | 108 | def addItem(self, item): 109 | self.itemList.append(item) 110 | 111 | def count(self): 112 | return len(self.itemList) 113 | 114 | def itemAt(self, index): 115 | if index >= 0 and index < len(self.itemList): 116 | return self.itemList[index] 117 | 118 | return None 119 | 120 | def takeAt(self, index): 121 | if index >= 0 and index < len(self.itemList): 122 | return self.itemList.pop(index) 123 | 124 | return None 125 | 126 | def expandingDirections(self): 127 | return QtCore.Qt.Orientations(QtCore.Qt.Orientation(0)) 128 | 129 | def heightForWidth(self, width): 130 | height = self.doLayout(QtCore.QRect(0, 0, width, 0), True) 131 | return height 132 | 133 | def setGeometry(self, rect): 134 | QtGui.QLayout.setGeometry(self, rect) 135 | self.doLayout(rect, False) 136 | 137 | def sizeHint(self): 138 | return self.minimumSize() 139 | 140 | def minimumSize(self): 141 | size = QtCore.QSize() 142 | 143 | for item in self.itemList: 144 | size = size.expandedTo(item.minimumSize()) 145 | 146 | size += QtCore.QSize(2*self.contentsMargins().top(), 147 | 2*self.contentsMargins().top()) 148 | return size 149 | 150 | def doLayout(self, rect, testOnly): 151 | x = rect.x() 152 | y = rect.y() 153 | lineHeight = 0 154 | 155 | for item in self.itemList: 156 | spaceX = self.spacing() 157 | spaceY = self.spacing() 158 | 159 | nextX = x + item.sizeHint().width() + spaceX 160 | if nextX - spaceX > rect.right() and lineHeight > 0: 161 | x = rect.x() 162 | y = y + lineHeight + spaceY 163 | nextX = x + item.sizeHint().width() + spaceX 164 | lineHeight = 0 165 | 166 | if not testOnly: 167 | item.setGeometry(QtCore.QRect(QtCore.QPoint(x, y), 168 | item.sizeHint())) 169 | 170 | x = nextX 171 | lineHeight = max(lineHeight, item.sizeHint().height()) 172 | 173 | return y + lineHeight - rect.y() 174 | -------------------------------------------------------------------------------- /noteorganiser/syntax.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import PySide.QtGui as QtGui 3 | import PySide.QtCore as QtCore 4 | 5 | 6 | class ModifiedMarkdownHighlighter(QtGui.QSyntaxHighlighter): 7 | 8 | def __init__(self, parent=None): 9 | QtGui.QSyntaxHighlighter.__init__(self, parent) 10 | 11 | # Initialise the highlighting rules 12 | self.highlightingRules = [] 13 | self.underliningRules = [] 14 | 15 | # Date rules 16 | dateFormat = QtGui.QTextCharFormat() 17 | dateFormat.setForeground(QtCore.Qt.darkGray) 18 | self.highlightingRules.append( 19 | (QtCore.QRegExp("\*{1,1}(\\d{1,2}/\\d{2,2}/\\d{4,4}\*{1,1}"), 20 | dateFormat)) 21 | 22 | # Italics rules 23 | italicsFormat = QtGui.QTextCharFormat() 24 | italicsFormat.setFontItalic(True) 25 | italicsFormat.setForeground(QtCore.Qt.darkGreen) 26 | self.highlightingRules.append( 27 | (QtCore.QRegExp("\*{1,1}([^\n^\*]+)\*{1,1}"), italicsFormat)) 28 | 29 | # Bold rules 30 | boldFormat = QtGui.QTextCharFormat() 31 | boldFormat.setForeground(QtCore.Qt.darkBlue) 32 | boldFormat.setFontWeight(QtGui.QFont.Bold) 33 | self.highlightingRules.append( 34 | (QtCore.QRegExp("\*{2,2}([^\n^\*]+)\*{2,2}"), boldFormat)) 35 | 36 | # Code rules 37 | self.codeFormat = QtGui.QTextCharFormat() 38 | self.codeFormat.setForeground(QtCore.Qt.darkBlue) 39 | self.highlightingRules.append( 40 | (QtCore.QRegExp("`{1,1}([^\n^`]+)`{1,1}"), self.codeFormat)) 41 | 42 | # Tags 43 | tagFormat = QtGui.QTextCharFormat() 44 | tagFormat.setForeground(QtCore.Qt.darkRed) 45 | self.highlightingRules.append( 46 | (QtCore.QRegExp( 47 | "^#\s?((\w(?:[-\w .]*\w)?)+)(,\s*(\w(?:[-\w .]*\w)?)+)*$"), 48 | tagFormat)) 49 | 50 | # Code blocks (several lines) 51 | self.blockStartExpression = QtCore.QRegExp("^~~~(\s.*)?$") 52 | self.blockEndExpression = QtCore.QRegExp("^~~~$") 53 | 54 | # Main title rule 55 | self.mainTitleUnderlineExpression = QtCore.QRegExp("^={2,}$") 56 | mainTitleFormat = QtGui.QTextCharFormat() 57 | mainTitleFormat.setForeground(QtCore.Qt.darkGreen) 58 | # Sections rule 59 | self.sectionUnderlineExpression = QtCore.QRegExp("^-{2,}$") 60 | sectionFormat = QtGui.QTextCharFormat() 61 | sectionFormat.setForeground(QtCore.Qt.darkBlue) 62 | self.underliningRules.extend( 63 | [(self.mainTitleUnderlineExpression, mainTitleFormat), 64 | (self.sectionUnderlineExpression, sectionFormat)]) 65 | 66 | def highlightBlock(self, text): 67 | # Deal first with simple expressions (one line) 68 | # Note: the _format syntax is there to avoid naming conflict with the 69 | # restricted word `format`. 70 | for pattern, _format in self.highlightingRules: 71 | expression = QtCore.QRegExp(pattern) 72 | index = expression.indexIn(text) 73 | while index >= 0: 74 | length = expression.matchedLength() 75 | self.setFormat(index, length, _format) 76 | index = expression.indexIn(text, index + length) 77 | 78 | # Deal with block type highlighting 79 | self.setCurrentBlockState(0) 80 | startIndex = 0 81 | if self.previousBlockState() != 1: 82 | startIndex = self.blockStartExpression.indexIn(text) 83 | 84 | while startIndex >= 0: 85 | endIndex = self.blockEndExpression.indexIn( 86 | text, startIndex) 87 | 88 | if endIndex == -1: 89 | self.setCurrentBlockState(1) 90 | blockLength = len(text) - startIndex 91 | elif endIndex == 0: 92 | # This is a symmetric code block 93 | if self.previousBlockState() == 0: 94 | self.setCurrentBlockState(1) 95 | blockLength = len(text) - startIndex 96 | else: 97 | blockLength = endIndex - startIndex + \ 98 | self.blockEndExpression.matchedLength() 99 | 100 | self.setFormat(startIndex, blockLength, 101 | self.codeFormat) 102 | startIndex = self.blockStartExpression.indexIn( 103 | text, startIndex + blockLength) 104 | 105 | # Match the underlines 106 | if startIndex == -1: # If outside of a block 107 | if self.previousBlockState() != 2: 108 | for pattern, _format in self.underliningRules: 109 | expression = QtCore.QRegExp(pattern) 110 | # Match the next line for underlines 111 | index = expression.indexIn( 112 | self.currentBlock().next().text()) 113 | if index >= 0: 114 | length = expression.matchedLength() 115 | self.setFormat(index, length, _format) 116 | index = expression.indexIn(text, index + length) 117 | self.setCurrentBlockState(2) 118 | # If the title has already been highlighted, the state is '2' 119 | else: 120 | for pattern, _format in self.underliningRules: 121 | expression = QtCore.QRegExp(pattern) 122 | # Match the next line for underlines 123 | index = expression.indexIn(text) 124 | if index >= 0: 125 | length = expression.matchedLength() 126 | self.setFormat(index, length, _format) 127 | index = expression.indexIn(text, index + length) 128 | -------------------------------------------------------------------------------- /noteorganiser/configuration.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. module:: configuration 3 | :synopsys: Recover the list of existing notebooks 4 | 5 | .. moduleauthor:: Benjamin Audren 6 | """ 7 | from __future__ import unicode_literals 8 | import os 9 | from noteorganiser.constants import EXTENSION 10 | 11 | from PySide import QtCore 12 | from PySide import QtGui 13 | 14 | 15 | def initialise(logger, force_folder_change=False): 16 | """ 17 | Platform independent recovery of the main folder and notebooks 18 | 19 | """ 20 | # Platform independent recovery of the home directory. 21 | home = os.path.expanduser("~") 22 | 23 | # Recover existing settings 24 | settings = QtCore.QSettings("audren", "NoteOrganiser") 25 | 26 | # Set the location of the output. Default is the home folder, but the user 27 | # can choose a cloud-synced folder instead. 28 | if settings.contains("home_folder") and not force_folder_change: 29 | main = settings.value("home_folder") 30 | else: 31 | # Bring popup to ask for a folder, with folder navigation 32 | dialog = QtGui.QFileDialog() 33 | dialog.setFileMode(QtGui.QFileDialog.Directory) 34 | dialog.setOption(QtGui.QFileDialog.ShowDirsOnly) 35 | text = "" 36 | if not force_folder_change: 37 | text += "Welcome to Note Organiser! " 38 | text += ("Please choose a folder to store your notes. " 39 | "Cancel for default.") 40 | if not force_folder_change: 41 | main = dialog.getExistingDirectory( 42 | None, text, home) 43 | else: 44 | main = dialog.getExistingDirectory( 45 | None, text, settings.value("home_folder")) 46 | if not main: 47 | main = os.path.join(home, '.noteorganiser') 48 | settings.setValue("home_folder", main) 49 | 50 | if settings.contains("display_empty"): 51 | if settings.value("display_empty") == "true": 52 | display_empty = True 53 | else: 54 | display_empty = False 55 | else: 56 | display_empty = True 57 | 58 | # Recursively search the main folder for notebooks or folders of notebooks 59 | # It also checks if the folder ".noteorganiser" exists, and creates it 60 | # otherwise. 61 | # folders will contain all the non-empty folders in the main. The method 62 | # search_folder_recursively will be called again when the user wants to 63 | # explore also the contents of this folder 64 | notebooks, folders = search_folder_recursively(logger, main, display_empty) 65 | 66 | # Return both the path to the folder where it is stored, and the list of 67 | # notebooks 68 | return main, notebooks, folders 69 | 70 | 71 | def search_folder_recursively(logger, main, display_empty=True): 72 | """ 73 | Search the main folder for notebooks and folders with notebooks 74 | 75 | Note that the returned notebooks and folders are flat (that is, folders is 76 | not a list that then contains all the subnotebooks. They are discarded, and 77 | only loaded if the folder is then clicked on). 78 | 79 | Parameters 80 | ---------- 81 | logger : logging module instance 82 | 83 | main : str 84 | folder to search 85 | 86 | display_empty : bool 87 | determines whether to return empty folders or not 88 | """ 89 | notebooks, folders = [], [] 90 | if os.path.isdir(main): 91 | logger.info("Main folder existed already") 92 | # If yes, check if there are already some notebooks 93 | for elem in os.listdir(main): 94 | if os.path.isfile(os.path.join(main, elem)): 95 | # If it is a valid file, append it to notebooks 96 | if elem[-len(EXTENSION):] == EXTENSION: 97 | logger.info("Found the file %s as a valid notebook" % elem) 98 | notebooks.append(elem) 99 | elif os.path.isdir(os.path.join(main, elem)): 100 | # Otherwise, check the folder for valid files, and append it to 101 | # folders in case there are some inside, or if display_empty is 102 | # set to True (by default). 103 | # If the folder is hidden (linux convention, with a leading 104 | # dot), ignore. TODO also determines if it is hidden as far as 105 | # Windows is concerned 106 | if elem[0] != '.': 107 | temp, _ = search_folder_recursively( 108 | logger, os.path.join(main, elem), display_empty) 109 | if temp or display_empty: 110 | folders.append(os.path.join(main, elem)) 111 | else: 112 | logger.info("Main folder non-existant: creating it now") 113 | os.mkdir(main) 114 | return notebooks, folders 115 | 116 | 117 | class Information(object): 118 | """storage of information across the application""" 119 | 120 | def __init__(self, logger, root, notebooks, folders): 121 | # Store the main variables 122 | # This is a reference to the global logger 123 | self.logger = logger 124 | # root stores the absolute path to the noteorganiser folder. It should 125 | # point to ~/.noteorganiser on a Unix type machine, and I don't know 126 | # where on a Windows. 127 | self.root = root 128 | # level stores the current path in the root folder (still in absolute 129 | # path, though) 130 | self.level = root 131 | 132 | # notebooks is the list of notebooks files (ending with EXTENSION), 133 | # found in "level". Folders contains the list of non-empty, non-hidden 134 | # folders in this directory. 135 | self.notebooks = notebooks 136 | self.folders = folders 137 | 138 | # Reference towards the currently edited/previewed notebook 139 | self.current_notebook = '' 140 | # Stores the SHA sum for every notebook, in order to avoid re-analyzing 141 | # the entire file for each filtering TODO 142 | self.sha = {} 143 | 144 | # get saved settings 145 | self.settings = QtCore.QSettings("audren", "NoteOrganiser") 146 | 147 | # Switch that holds the property to either display or hide empty 148 | # folders in the shelves 149 | if self.settings.contains("display_empty"): 150 | if self.settings.value("display_empty") == "true": 151 | self.display_empty = True 152 | else: 153 | self.display_empty = False 154 | else: 155 | self.display_empty = True 156 | 157 | # commandline for the external editor 158 | self.externalEditor = '' 159 | if self.settings.contains("externalEditor"): 160 | self.externalEditor = self.settings.value("externalEditor") 161 | 162 | # automatically refresh the editor if file changes 163 | if self.settings.contains("refreshEditor"): 164 | if self.settings.value("refreshEditor") == "true": 165 | self.refreshEditor = True 166 | else: 167 | self.refreshEditor = False 168 | else: 169 | self.refreshEditor = False 170 | 171 | # Switch to use Table of Content in HTML-Output 172 | if self.settings.contains("use_TOC"): 173 | if self.settings.value("use_TOC") == "true": 174 | self.use_TOC = True 175 | else: 176 | self.use_TOC = False 177 | else: 178 | self.use_TOC = False 179 | -------------------------------------------------------------------------------- /noteorganiser/assets/style/fonts/inconsolata/specimen_files/specimen_stylesheet.css: -------------------------------------------------------------------------------- 1 | @import url('grid_12-825-55-15.css'); 2 | 3 | /* 4 | CSS Reset by Eric Meyer - Released under Public Domain 5 | http://meyerweb.com/eric/tools/css/reset/ 6 | */ 7 | html, body, div, span, applet, object, iframe, 8 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 9 | a, abbr, acronym, address, big, cite, code, 10 | del, dfn, em, font, img, ins, kbd, q, s, samp, 11 | small, strike, strong, sub, sup, tt, var, 12 | b, u, i, center, dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, table, 14 | caption, tbody, tfoot, thead, tr, th, td 15 | {margin: 0;padding: 0;border: 0;outline: 0; 16 | font-size: 100%;vertical-align: baseline; 17 | background: transparent;} 18 | body {line-height: 1;} 19 | ol, ul {list-style: none;} 20 | blockquote, q {quotes: none;} 21 | blockquote:before, blockquote:after, 22 | q:before, q:after {content: ''; content: none;} 23 | :focus {outline: 0;} 24 | ins {text-decoration: none;} 25 | del {text-decoration: line-through;} 26 | table {border-collapse: collapse;border-spacing: 0;} 27 | 28 | 29 | 30 | 31 | body { 32 | color: #000; 33 | background-color: #dcdcdc; 34 | } 35 | 36 | a { 37 | text-decoration: none; 38 | color: #1883ba; 39 | } 40 | 41 | h1{ 42 | font-size: 32px; 43 | font-weight: normal; 44 | font-style: normal; 45 | margin-bottom: 18px; 46 | } 47 | 48 | h2{ 49 | font-size: 18px; 50 | } 51 | 52 | #container { 53 | width: 865px; 54 | margin: 0px auto; 55 | } 56 | 57 | 58 | #header { 59 | padding: 20px; 60 | font-size: 36px; 61 | background-color: #000; 62 | color: #fff; 63 | } 64 | 65 | #header span { 66 | color: #666; 67 | } 68 | #main_content { 69 | background-color: #fff; 70 | padding: 60px 20px 20px; 71 | } 72 | 73 | 74 | #footer p { 75 | margin: 0; 76 | padding-top: 10px; 77 | padding-bottom: 50px; 78 | color: #333; 79 | font: 10px Arial, sans-serif; 80 | } 81 | 82 | .tabs { 83 | width: 100%; 84 | height: 31px; 85 | background-color: #444; 86 | } 87 | .tabs li { 88 | float: left; 89 | margin: 0; 90 | overflow: hidden; 91 | background-color: #444; 92 | } 93 | .tabs li a { 94 | display: block; 95 | color: #fff; 96 | text-decoration: none; 97 | font: bold 11px/11px 'Arial'; 98 | text-transform: uppercase; 99 | padding: 10px 15px; 100 | border-right: 1px solid #fff; 101 | } 102 | 103 | .tabs li a:hover { 104 | background-color: #00b3ff; 105 | 106 | } 107 | 108 | .tabs li.active a { 109 | color: #000; 110 | background-color: #fff; 111 | } 112 | 113 | 114 | 115 | div.huge { 116 | 117 | font-size: 300px; 118 | line-height: 1em; 119 | padding: 0; 120 | letter-spacing: -.02em; 121 | overflow: hidden; 122 | } 123 | div.glyph_range { 124 | font-size: 72px; 125 | line-height: 1.1em; 126 | } 127 | 128 | .size10{ font-size: 10px; } 129 | .size11{ font-size: 11px; } 130 | .size12{ font-size: 12px; } 131 | .size13{ font-size: 13px; } 132 | .size14{ font-size: 14px; } 133 | .size16{ font-size: 16px; } 134 | .size18{ font-size: 18px; } 135 | .size20{ font-size: 20px; } 136 | .size24{ font-size: 24px; } 137 | .size30{ font-size: 30px; } 138 | .size36{ font-size: 36px; } 139 | .size48{ font-size: 48px; } 140 | .size60{ font-size: 60px; } 141 | .size72{ font-size: 72px; } 142 | .size90{ font-size: 90px; } 143 | 144 | 145 | .psample_row1 { height: 120px;} 146 | .psample_row1 { height: 120px;} 147 | .psample_row2 { height: 160px;} 148 | .psample_row3 { height: 160px;} 149 | .psample_row4 { height: 160px;} 150 | 151 | .psample { 152 | overflow: hidden; 153 | position: relative; 154 | } 155 | .psample p { 156 | line-height: 1.3em; 157 | display: block; 158 | overflow: hidden; 159 | margin: 0; 160 | } 161 | 162 | .psample span { 163 | margin-right: .5em; 164 | } 165 | 166 | .white_blend { 167 | width: 100%; 168 | height: 61px; 169 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVkAAAA9CAYAAAAH4BojAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAO1JREFUeNrs3TsKgFAMRUE/eer+NxztxMYuEWQG3ECKwwUF58ycAKixOAGAyAKILAAiCyCyACILgMgCiCyAyAIgsgAiCyCyAIgsgMgCiCwAIgsgsgAiC4DIAogsACIL0CWuZ3UGgLrIhjMA1EV2OAOAJQtgyQLwjOzmDAAiCyCyAIgsQFtkd2cAEFkAkQVAZAHaIns4A4AlC2DJAiCyACILILIAiCzAV5H1dQGAJQsgsgCILIDIAvwisl58AViyAJYsACILILIAIgvAe2T9EhxAZAFEFgCRBeiL7HAGgLrIhjMAWLIAliwAt1OAAQDwygTBulLIlQAAAABJRU5ErkJggg==); 170 | position: absolute; 171 | bottom: 0; 172 | } 173 | .black_blend { 174 | width: 100%; 175 | height: 61px; 176 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVkAAAA9CAYAAAAH4BojAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAPJJREFUeNrs3TEKhTAQRVGjibr/9QoxhY2N3Ywo50A28IrLwP9g6b1PAMSYTQAgsgAiC4DIAogsgMgCILIAIgsgsgCILIDIAogsACILILIAIguAyAKILIDIAiCyACILgMgCZCnjLWYAiFGvB0BQZJsZAFyyAC5ZAO6RXc0AILIAIguAyAKkRXYzA4DIAogsACILkBbZ3QwALlkAlywAIgsgsgAiC4DIArwVWf8uAHDJAogsACILILIAv4isH74AXLIALlkARBZAZAFEFoDnyPokOIDIAogsACILkBfZZgaAuMhWMwC4ZAE+p4x3mAEgxinAAJ+XBbPWGkwAAAAAAElFTkSuQmCC); 177 | position: absolute; 178 | bottom: 0; 179 | } 180 | .fullreverse { 181 | background: #000 !important; 182 | color: #fff !important; 183 | margin-left: -20px; 184 | padding-left: 20px; 185 | margin-right: -20px; 186 | padding-right: 20px; 187 | padding: 20px; 188 | margin-bottom:0; 189 | } 190 | 191 | 192 | .sample_table td { 193 | padding-top: 3px; 194 | padding-bottom:5px; 195 | padding-left: 5px; 196 | vertical-align: middle; 197 | line-height: 1.2em; 198 | } 199 | 200 | .sample_table td:first-child { 201 | background-color: #eee; 202 | text-align: right; 203 | padding-right: 5px; 204 | padding-left: 0; 205 | padding: 5px; 206 | font: 11px/12px "Courier New", Courier, mono; 207 | } 208 | 209 | code { 210 | white-space: pre; 211 | background-color: #eee; 212 | display: block; 213 | padding: 10px; 214 | margin-bottom: 18px; 215 | overflow: auto; 216 | } 217 | 218 | 219 | .bottom,.last {margin-bottom:0 !important; padding-bottom:0 !important;} 220 | 221 | .box { 222 | padding: 18px; 223 | margin-bottom: 18px; 224 | background: #eee; 225 | } 226 | 227 | .reverse,.reversed { background: #000 !important;color: #fff !important; border: none !important;} 228 | 229 | #bodycomparison { 230 | position: relative; 231 | overflow: hidden; 232 | font-size: 72px; 233 | height: 90px; 234 | white-space: nowrap; 235 | } 236 | 237 | #bodycomparison div{ 238 | font-size: 72px; 239 | line-height: 90px; 240 | display: inline; 241 | margin: 0 15px 0 0; 242 | padding: 0; 243 | } 244 | 245 | #bodycomparison div span{ 246 | font: 10px Arial; 247 | position: absolute; 248 | left: 0; 249 | } 250 | #xheight { 251 | float: none; 252 | position: absolute; 253 | color: #d9f3ff; 254 | font-size: 72px; 255 | line-height: 90px; 256 | } 257 | 258 | .fontbody { 259 | position: relative; 260 | } 261 | .arialbody{ 262 | font-family: Arial; 263 | position: relative; 264 | } 265 | .verdanabody{ 266 | font-family: Verdana; 267 | position: relative; 268 | } 269 | .georgiabody{ 270 | font-family: Georgia; 271 | position: relative; 272 | } 273 | 274 | /* @group Layout page 275 | */ 276 | 277 | #layout h1 { 278 | font-size: 36px; 279 | line-height: 42px; 280 | font-weight: normal; 281 | font-style: normal; 282 | } 283 | 284 | #layout h2 { 285 | font-size: 24px; 286 | line-height: 23px; 287 | font-weight: normal; 288 | font-style: normal; 289 | } 290 | 291 | #layout h3 { 292 | font-size: 22px; 293 | line-height: 1.4em; 294 | margin-top: 1em; 295 | font-weight: normal; 296 | font-style: normal; 297 | } 298 | 299 | 300 | #layout p.byline { 301 | font-size: 12px; 302 | margin-top: 18px; 303 | line-height: 12px; 304 | margin-bottom: 0; 305 | } 306 | #layout p { 307 | font-size: 14px; 308 | line-height: 21px; 309 | margin-bottom: .5em; 310 | } 311 | 312 | #layout p.large{ 313 | font-size: 18px; 314 | line-height: 26px; 315 | } 316 | 317 | #layout .sidebar p{ 318 | font-size: 12px; 319 | line-height: 1.4em; 320 | } 321 | 322 | #layout p.caption { 323 | font-size: 10px; 324 | margin-top: -16px; 325 | margin-bottom: 18px; 326 | } 327 | 328 | /* @end */ 329 | 330 | /* @group Glyphs */ 331 | 332 | #glyph_chart div{ 333 | background-color: #d9f3ff; 334 | color: black; 335 | float: left; 336 | font-size: 36px; 337 | height: 1.2em; 338 | line-height: 1.2em; 339 | margin-bottom: 1px; 340 | margin-right: 1px; 341 | text-align: center; 342 | width: 1.2em; 343 | position: relative; 344 | padding: .6em .2em .2em; 345 | } 346 | 347 | #glyph_chart div p { 348 | position: absolute; 349 | left: 0; 350 | top: 0; 351 | display: block; 352 | text-align: center; 353 | font: bold 9px Arial, sans-serif; 354 | background-color: #3a768f; 355 | width: 100%; 356 | color: #fff; 357 | padding: 2px 0; 358 | } 359 | 360 | 361 | #glyphs h1 { 362 | font-family: Arial, sans-serif; 363 | } 364 | /* @end */ 365 | 366 | /* @group Installing */ 367 | 368 | #installing { 369 | font: 13px Arial, sans-serif; 370 | } 371 | 372 | #installing p, 373 | #glyphs p{ 374 | line-height: 1.2em; 375 | margin-bottom: 18px; 376 | font: 13px Arial, sans-serif; 377 | } 378 | 379 | 380 | 381 | #installing h3{ 382 | font-size: 15px; 383 | margin-top: 18px; 384 | } 385 | 386 | /* @end */ 387 | 388 | #rendering h1 { 389 | font-family: Arial, sans-serif; 390 | } 391 | .render_table td { 392 | font: 11px "Courier New", Courier, mono; 393 | vertical-align: middle; 394 | } 395 | 396 | 397 | -------------------------------------------------------------------------------- /noteorganiser/widgets.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import re 4 | import os 5 | from PySide import QtGui 6 | from PySide import QtCore 7 | 8 | os.environ['QT_API'] = 'pyside' 9 | import qtawesome 10 | 11 | from .utils import MultiCompleter 12 | 13 | 14 | class PicButton(QtGui.QPushButton): 15 | """Button with a picture""" 16 | deleteNotebookSignal = QtCore.Signal(str) 17 | deleteFolderSignal = QtCore.Signal(str) 18 | previewSignal = QtCore.Signal(str) 19 | 20 | def __init__(self, pixmap, text, style, parent=None): 21 | QtGui.QPushButton.__init__(self, parent) 22 | self.parent = parent 23 | self.label = str(text) 24 | # Define the tooltip 25 | self.setToolTip(self.label) 26 | self.pixmap = pixmap 27 | self.style = style 28 | 29 | # Default fontsize 30 | self.default = 9 31 | self.fontsize = self.default 32 | 33 | # Define behaviour under right click 34 | self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) 35 | delete = QtGui.QAction(self) 36 | delete.setText("delete") 37 | delete.triggered.connect(self.removeButton) 38 | self.addAction(delete) 39 | 40 | # use the preview action only on notebook 41 | if self.style == 'notebook': 42 | # Define behaviour for direct preview 43 | preview = QtGui.QAction(self) 44 | preview.setText("preview") 45 | preview.triggered.connect(self.previewNotebook) 46 | self.addAction(preview) 47 | 48 | def paintEvent(self, event): 49 | painter = QtGui.QPainter(self) 50 | painter.setFont(QtGui.QFont('unicode', self.fontsize)) 51 | 52 | # If the width of the text is too large, reduce the fontsize 53 | metrics = QtGui.QFontMetrics(self.font()) 54 | if metrics.width(self.label) > 76: 55 | self.fontsize = 7 56 | painter.setFont(QtGui.QFont('unicode', self.fontsize)) 57 | 58 | # If the width is still too large, elide the text 59 | elided = metrics.elidedText( 60 | self.label, QtCore.Qt.ElideRight, 90) 61 | 62 | painter.drawPixmap(event.rect(), self.pixmap) 63 | if self.style == 'notebook': 64 | painter.translate(42, 102) 65 | painter.rotate(-90) 66 | elif self.style == 'folder': 67 | painter.translate(10, 100+self.default-self.fontsize) 68 | painter.drawText(event.rect(), elided) 69 | 70 | def sizeHint(self): 71 | return self.pixmap.size() 72 | 73 | def mouseReleaseEvent(self, ev): 74 | """Define a behaviour under click""" 75 | # only fire event, when left button is clicked 76 | if ev.button() == QtCore.Qt.LeftButton: 77 | # check for shift-key 78 | modifiers = QtGui.QApplication.keyboardModifiers() 79 | if modifiers == QtCore.Qt.ShiftModifier: 80 | self.previewNotebook() 81 | else: 82 | self.click() 83 | 84 | def removeButton(self): 85 | """Delegate to the parent to deal with the situation""" 86 | if self.style == 'notebook': 87 | self.deleteNotebookSignal.emit(self.label) 88 | else: 89 | self.deleteFolderSignal.emit(self.label) 90 | 91 | def previewNotebook(self): 92 | """emmit signal to preview the current notebook""" 93 | self.previewSignal.emit(self.label) 94 | 95 | 96 | class VerticalScrollArea(QtGui.QScrollArea): 97 | """Implementation of a purely vertical scroll area""" 98 | 99 | def __init__(self, parent=None): 100 | QtGui.QScrollArea.__init__(self, parent) 101 | self.setWidgetResizable(True) 102 | self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 103 | self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) 104 | self.verticalScrollBar().setFocusPolicy(QtCore.Qt.StrongFocus) 105 | 106 | def eventFilter(self, item, event): 107 | """ 108 | This works because setWidget installs an eventFilter on the widget""" 109 | if (item and item == self.widget() 110 | and event.type() == QtCore.QEvent.Resize): 111 | self.setMinimumWidth( 112 | self.widget().minimumSizeHint().width() + 113 | self.verticalScrollBar().width()) 114 | return QtGui.QScrollArea.eventFilter(self, item, event) 115 | 116 | 117 | class LineEditWithClearButton(QtGui.QLineEdit): 118 | """a line edit widget that shows a clear button if there is text""" 119 | buttonClicked = QtCore.Signal(bool) 120 | 121 | def __init__(self, parent=None): 122 | QtGui.QLineEdit.__init__(self, parent) 123 | 124 | self.clearButton = QtGui.QPushButton('x', self) 125 | palette = QtGui.QPalette() 126 | palette.setColor(QtGui.QPalette.ButtonText, QtCore.Qt.red) 127 | self.clearButton.setPalette(palette) 128 | self.clearButton.setStyleSheet('border: 0px;' 129 | 'padding: 0px;' 130 | 'padding-right: 3px;' 131 | 'font-weight: bold;') 132 | self.clearButton.setCursor(QtCore.Qt.ArrowCursor) 133 | self.clearButton.setFocusPolicy(QtCore.Qt.NoFocus) 134 | self.clearButton.setVisible(False) 135 | self.clearButton.clicked.connect(self.clear) 136 | self.textChanged.connect(self.showClearButton) 137 | 138 | frameWidth = self.style().pixelMetric( 139 | QtGui.QStyle.PM_DefaultFrameWidth) 140 | buttonSize = self.clearButton.sizeHint() 141 | 142 | self.setStyleSheet('QLineEdit {padding-right: %dpx; }' % 143 | (buttonSize.width() + frameWidth)) 144 | self.setMinimumSize(max(self.minimumSizeHint().width(), 145 | buttonSize.width() + frameWidth*2), 146 | max(self.minimumSizeHint().height(), 147 | buttonSize.height() + frameWidth*2)) 148 | 149 | def resizeEvent(self, event): 150 | """move the clear button with the widget""" 151 | buttonSize = self.clearButton.sizeHint() 152 | frameWidth = self.style().pixelMetric( 153 | QtGui.QStyle.PM_DefaultFrameWidth) 154 | self.clearButton.move(self.rect().right() - frameWidth - 155 | buttonSize.width(), (self.rect().bottom() - 156 | buttonSize.height() + 1)/2) 157 | QtGui.QLineEdit.resizeEvent(self, event) 158 | 159 | def showClearButton(self): 160 | """show the clear button if there's text""" 161 | self.clearButton.setVisible(len(self.text())) 162 | 163 | 164 | class TagCompletion(QtGui.QLineEdit): 165 | """ a QLineEdit with a QCompleter to add tags from the current file """ 166 | 167 | def __init__(self, tags, parent=None): 168 | QtGui.QLineEdit.__init__(self, parent) 169 | self.parent = parent 170 | self.initTagCompletion(tags) 171 | self.initDownButton() 172 | 173 | def initTagCompletion(self, tags=None): 174 | """add a multi-item completer to the given widget""" 175 | if tags is None: 176 | tags = [] 177 | 178 | tags = sorted(tags) 179 | self.completer = MultiCompleter(list(tags), self) 180 | self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) 181 | self.setCompleter(self.completer) 182 | self.returnPressed.connect(self.onReturnPressed) 183 | 184 | def initDownButton(self): 185 | """add a little down-arrow to start completion 186 | (list all available tags)""" 187 | downIcon = qtawesome.icon('fa.sort-down') 188 | self.downButton = QtGui.QPushButton(downIcon, '', self) 189 | self.downButton.setStyleSheet('border: 0px;' 190 | 'padding: 0px;') 191 | self.downButton.setCursor(QtCore.Qt.ArrowCursor) 192 | self.downButton.setFocusPolicy(QtCore.Qt.NoFocus) 193 | self.downButton.clicked.connect(self.onDownPressed) 194 | frameWidth = self.style().pixelMetric( 195 | QtGui.QStyle.PM_DefaultFrameWidth) 196 | buttonSize = self.downButton.sizeHint() 197 | 198 | self.setStyleSheet('QLineEdit {padding-right: %dpx; }' % 199 | (buttonSize.width() + frameWidth)) 200 | self.setMinimumSize(max(self.minimumSizeHint().width(), 201 | buttonSize.width() + frameWidth*2), 202 | max(self.minimumSizeHint().height(), 203 | buttonSize.height() + frameWidth*2)) 204 | 205 | def resizeEvent(self, event): 206 | """move the button with the widget""" 207 | buttonSize = self.downButton.sizeHint() 208 | frameWidth = self.style().pixelMetric( 209 | QtGui.QStyle.PM_DefaultFrameWidth) 210 | self.downButton.move(self.rect().right() - frameWidth - 211 | buttonSize.width(), (self.rect().bottom() - 212 | buttonSize.height() + 1)/2) 213 | QtGui.QLineEdit.resizeEvent(self, event) 214 | 215 | def onReturnPressed(self): 216 | """ get the first item from the completer """ 217 | 218 | self.completer.setCompletionPrefix(self.text()) 219 | if self.completer.completionModel().rowCount(): 220 | self.setText(self.completer.currentCompletion()) 221 | 222 | def onDownPressed(self): 223 | """ 224 | down Button was pressed 225 | 226 | show completion dropdown 227 | """ 228 | self.completer.setCompletionPrefix(self.text()) 229 | self.completer.complete() 230 | 231 | def getTextWithNormalizedSeparators(self): 232 | """ 233 | get the text with all separators replaced by separators[0] 234 | 235 | separators[0] is normally ','. 236 | This corresponds to the separator used for tags in the markdown files 237 | """ 238 | 239 | return re.sub(r'[{0}]'.format(''.join(self.completer.separators)), 240 | self.completer.separators[0], self.text()) 241 | -------------------------------------------------------------------------------- /noteorganiser/text_processing.py: -------------------------------------------------------------------------------- 1 | # A post is considered to be extracted from an existing file, and always 2 | # consists in a title, followed by a line of '------------', followed 3 | # immediatly by the tags with a leading #. It is a list of strings, where empty 4 | # ones were removed. 5 | # If it is a valid post, it has at minima 4 lines (title, line of -, tags, 6 | # date) 7 | # They should be extracted recursively, and fed to the different routines 8 | # `_from_post`. 9 | # Note that all doctests are commented, and now executed in the py.test suite, 10 | # for compatibility reasons between py2.7 and py3.3 11 | from __future__ import unicode_literals 12 | from datetime import date 13 | from collections import Counter 14 | from collections import OrderedDict as od 15 | import re 16 | import io 17 | 18 | 19 | def is_valid_post(post): 20 | """ 21 | Check that it has all the required arguments 22 | 23 | If the post is valid, the function returns True. Otherwise, an 24 | MarkdownSyntaxError is raised with a description of the problem. 25 | 26 | """ 27 | # remove all blank lines 28 | temp = [e for e in post if e] 29 | if len(temp) < 4: 30 | raise MarkdownSyntaxError("Post contains under four lines", post) 31 | else: 32 | # Recover the index of the line of dashes, in case of long titles 33 | index = 0 34 | dashes = [e for e in temp if re.match('^-{2,}$', e)] 35 | try: 36 | index = temp.index(dashes[0]) 37 | except IndexError: 38 | raise MarkdownSyntaxError("Post does not contain dashes", post) 39 | if index: 40 | if not temp[0]: 41 | raise MarkdownSyntaxError("Post title is empty", post) 42 | if not re.match('^#.*$', temp[index+1]): 43 | raise MarkdownSyntaxError( 44 | "Tags were not found after the dashes", post) 45 | match = re.match( 46 | r"^\*[0-9]{2}/[0-1][0-9]/[0-9]{4}\*$", 47 | temp[index+2]) 48 | if not match: 49 | raise MarkdownSyntaxError( 50 | "The date could not be read", temp) 51 | return True 52 | 53 | 54 | def extract_tags_from_post(post): 55 | """ 56 | Recover the tags from an extracted post 57 | 58 | .. note:: 59 | 60 | No tests are being done to ensure that the third line exists, as only 61 | valid posts, determined with :func:`is_valid_post` are sent to this 62 | routine. 63 | 64 | """ 65 | tag_line = post[2].strip() 66 | if tag_line and tag_line[0] == '#': 67 | tags = [elem.strip().lower() for elem in tag_line[1:].split(',')] 68 | 69 | if any(tags): 70 | return tags, post[:2]+post[3:] 71 | else: 72 | raise MarkdownSyntaxError("No tags specified in the post", post) 73 | 74 | 75 | def extract_title_from_post(post): 76 | """ 77 | Recover the title from an extracted post 78 | 79 | """ 80 | return post[0] 81 | 82 | 83 | def extract_date_from_post(post): 84 | """ 85 | Recover the date from an extracted post, and return the correct post 86 | 87 | """ 88 | match = re.match( 89 | r"\*([0-9]{2})/([0-1][0-9])/([0-9]{4})\*", 90 | post[2]) 91 | if match: 92 | assert len(match.groups()) == 3 93 | day, month, year = match.groups() 94 | extracted_date = date(int(year), int(month), int(day)) 95 | return extracted_date, post[:2]+post[3:] 96 | else: 97 | raise MarkdownSyntaxError("No date found in the post", post) 98 | 99 | 100 | def normalize_post(post): 101 | """ 102 | Perform normalization of the input post 103 | 104 | - If a title has several lines, merge them 105 | - If there are missing/added blank lines in the headers, remove them 106 | """ 107 | # Remove trailing \n 108 | post = [line.rstrip('\n') for line in post] 109 | 110 | # Recover the dashline (title of the post) 111 | dashes = [e for e in post if re.match('^-{2,}$', e)] 112 | dashline_index = post.index(dashes[0]) 113 | 114 | title = ' '.join([post[index] for index in range(dashline_index)]) 115 | normalized_post = [title]+[post[dashline_index]] 116 | 117 | # Recover the tag line 118 | tags = [e for e in post if re.match('^\#(.*)$', e)] 119 | tag_line_index = post.index(tags[0]) 120 | normalized_post.append(post[tag_line_index]) 121 | 122 | # Recover the date 123 | dates = [e for e in post if re.match( 124 | r"\*([0-9]{2})/([0-1][0-9])/([0-9]{4})\*", e)] 125 | date_line_index = post.index(dates[0]) 126 | normalized_post.append(post[date_line_index]) 127 | 128 | # Append the rest, starting from the first non-empty line 129 | non_empty = [e for e in post[date_line_index+1:] if e] 130 | if non_empty: 131 | non_empty_index = post.index(non_empty[0]) 132 | normalized_post.extend(post[non_empty_index:]) 133 | 134 | return normalized_post 135 | 136 | 137 | def extract_corpus_from_post(post): 138 | """ 139 | Recover the whole content of a post 140 | 141 | """ 142 | return post[2:] 143 | 144 | 145 | def extract_title_and_posts_from_text(text): 146 | """ 147 | From an entire text (array), recover each posts and the file's title 148 | 149 | """ 150 | # Make a first pass to recover the title (first line that is underlined 151 | # with = signs) and the indices of the dash, that signals a new post. 152 | post_starting_indices = [] 153 | has_title = False 154 | for index, line in enumerate(text): 155 | # Remove white lines at the beginning 156 | if not line.strip(): 157 | continue 158 | if re.match('^={2,}$', line) and not has_title: 159 | title = ' '.join(text[:index]).strip() 160 | has_title = True 161 | if re.match('^-{2,}$', line) and line[0] == '-': 162 | # Check that the lines surrounding this line of dashes are 163 | # non-empty, otherwise it could be the beginning or end of a table. 164 | previous_line = text[index-1].rstrip('\n') 165 | if index+1 < len(text)-1: 166 | next_line = text[index+1].rstrip('\n') 167 | if previous_line and next_line: 168 | # find the latest non empty line 169 | for backward_index in range(1, 10): 170 | if not text[index-backward_index].strip(): 171 | post_starting_indices.append( 172 | index-backward_index+1) 173 | break 174 | 175 | if not has_title: 176 | raise MarkdownSyntaxError( 177 | "You should specify a title to your file" 178 | ", underlined with = signs", []) 179 | number_of_posts = len(post_starting_indices) 180 | 181 | # Create post_indices such that it stores all the post, so the starting and 182 | # ending index of each post 183 | post_indices = [] 184 | for index, elem in enumerate(post_starting_indices): 185 | if index < number_of_posts - 1: 186 | post_indices.append([elem, post_starting_indices[index+1]]) 187 | else: 188 | post_indices.append([elem, len(text)]) 189 | 190 | posts = [] 191 | for elem in post_indices: 192 | start, end = elem 193 | posts.append(text[start:end]) 194 | 195 | # Normalize them all 196 | for index, post in enumerate(posts): 197 | posts[index] = normalize_post(post) 198 | assert is_valid_post(posts[index]) is True 199 | 200 | return title, posts 201 | 202 | 203 | def post_to_markdown(post): 204 | """ 205 | Write the markdown for a given post 206 | 207 | post : list 208 | lines constituting the post 209 | 210 | """ 211 | title = post[0] 212 | text = ["", "
", 213 | "## %s {.blog-post-title}" % title, ""] 214 | 215 | tags, post = extract_tags_from_post(post) 216 | edit_date, post = extract_date_from_post(post) 217 | text.extend([""]) 221 | corpus = extract_corpus_from_post(post) 222 | text.extend(corpus) 223 | text.extend(["
", "", ""]) 224 | 225 | return text, tags 226 | 227 | 228 | def from_notes_to_markdown(path, input_tags=()): 229 | """ 230 | From a file, given tags, produce an output markdown file. 231 | 232 | This will then be interpreted with the pandoc library into html. 233 | 234 | ..note:: 235 | 236 | so far the date is ignored. 237 | 238 | Returns 239 | ------- 240 | markdown : list 241 | entire markdown text 242 | tags : list of tuples 243 | list of tags extracted from the text, with their importance 244 | """ 245 | # Create the array to return 246 | text = io.open(path, 'r', encoding='utf-8', errors='replace').readlines() 247 | title, posts = extract_title_and_posts_from_text(text) 248 | markdown = ["
", 249 | "# %s {.blog-title}" % title, "
", "", 250 | "
", "
"] 251 | extracted_tags = [] 252 | for post in posts: 253 | text, tags = post_to_markdown(post) 254 | if all([tag in tags for tag in input_tags]): 255 | # Store the recovered tags 256 | extracted_tags.extend(tags) 257 | markdown.extend(text) 258 | 259 | markdown.extend(["
", "
"]) 260 | cleaned_tags = sort_tags(extracted_tags) 261 | return markdown, cleaned_tags 262 | 263 | 264 | def sort_tags(source): 265 | """ 266 | return a sorted version of source, with the biggests tags first 267 | """ 268 | output = od(sorted(Counter([e for e in source]).items(), 269 | key=lambda t: -t[1])) 270 | return output 271 | 272 | 273 | def create_post_from_entry(title, tags, corpus): 274 | """ 275 | Create a string containing the post given user's input 276 | 277 | """ 278 | text = [title] 279 | text.append('\n%s\n' % ''.join(['-' for _ in range(len(title))])) 280 | text.append('# %s\n' % ', '.join(tags)) 281 | text.append('\n*%s*\n\n' % date.today().strftime("%d/%m/%Y")) 282 | text.append(corpus+'\n') 283 | return ''.join(text) 284 | 285 | 286 | def create_image_markdown(filename): 287 | """ 288 | Create a valid markdown string that presents the image given as a filename 289 | """ 290 | 291 | text = "![]({0})".format(filename) 292 | return text 293 | 294 | 295 | class MarkdownSyntaxError(ValueError): 296 | 297 | def __init__(self, message, post): 298 | ValueError.__init__(self, message+':\n\n' + 299 | '\n'.join([' %s' % e for e in post])) 300 | -------------------------------------------------------------------------------- /noteorganiser/popups.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from PySide import QtGui 3 | from PySide import QtCore 4 | import os 5 | 6 | from .constants import EXTENSION 7 | from .widgets import TagCompletion 8 | import noteorganiser.text_processing as tp 9 | 10 | 11 | class Dialog(QtGui.QDialog): 12 | """ 13 | Model for dialogs in Note Organiser (pop-up windows) 14 | 15 | """ 16 | def __init__(self, parent=None): 17 | """Define the shortcuts""" 18 | QtGui.QDialog.__init__(self, parent) 19 | self.parent = parent 20 | self.info = parent.info 21 | self.log = parent.log 22 | 23 | # Define Ctrl+W to close it, and overwrite Esc 24 | QtGui.QShortcut(QtGui.QKeySequence('Ctrl+W'), 25 | self, self.clean_reject) 26 | QtGui.QShortcut(QtGui.QKeySequence('Esc'), 27 | self, self.clean_reject) 28 | 29 | self.setLayout(QtGui.QVBoxLayout()) 30 | 31 | def clean_accept(self): 32 | """Logging the closing of the popup""" 33 | self.log.info("%s form suceeded!" % self.__class__.__name__) 34 | self.accept() 35 | 36 | def clean_reject(self): 37 | """Logging the rejection of the popup""" 38 | self.log.info("Aborting %s form" % self.__class__.__name__) 39 | self.reject() 40 | 41 | def translate(self, string): 42 | """Temporary fix for unicode tr problems""" 43 | return string 44 | 45 | 46 | class NewNotebook(Dialog): 47 | 48 | def __init__(self, parent=None): 49 | Dialog.__init__(self, parent) 50 | self.names = [os.path.splitext(elem)[0] 51 | for elem in self.info.notebooks] 52 | self.initUI() 53 | 54 | def initUI(self): 55 | self.log.info("Creating a 'New Notebook' form") 56 | 57 | self.setWindowTitle("New notebook") 58 | 59 | # Define the fields: 60 | # Name (text field) 61 | # type (so far, standard) 62 | formLayout = QtGui.QFormLayout() 63 | self.nameLineEdit = QtGui.QLineEdit() 64 | # Query the type of notebook 65 | self.notebookType = QtGui.QComboBox() 66 | self.notebookType.addItem("Standard") 67 | 68 | formLayout.addRow(self.translate("Notebook's &name:"), self.nameLineEdit) 69 | formLayout.addRow(self.translate("&Notebook's &type:"), self.notebookType) 70 | self.layout().addLayout(formLayout) 71 | 72 | hboxLayout = QtGui.QHBoxLayout() 73 | 74 | # Add the "Create" button, as a confirmation, and the "Cancel" one 75 | self.createButton = QtGui.QPushButton("&Create") 76 | self.createButton.clicked.connect(self.createNotebook) 77 | self.cancelButton = QtGui.QPushButton("C&ancel") 78 | self.cancelButton.clicked.connect(self.clean_reject) 79 | 80 | # Add a spacer before so that the button do not stretch 81 | hboxLayout.addStretch() 82 | hboxLayout.addWidget(self.createButton) 83 | hboxLayout.addWidget(self.cancelButton) 84 | self.layout().addLayout(hboxLayout) 85 | 86 | # Create a status bar 87 | self.statusBar = QtGui.QStatusBar(self) 88 | self.layout().addWidget(self.statusBar) 89 | 90 | def createNotebook(self): 91 | """Query the entry fields and append the notebook list""" 92 | desired_name = str(self.nameLineEdit.text()) 93 | self.log.info("Desired Notebook name: "+desired_name) 94 | if not desired_name or len(desired_name) < 2: 95 | self.statusBar.showMessage("name too short", 2000) 96 | self.log.info("name rejected: too short") 97 | else: 98 | if desired_name in self.names: 99 | self.statusBar.showMessage("name already used", 2000) 100 | self.log.info("name rejected: already used") 101 | else: 102 | # Actually creating the notebook 103 | self.info.notebooks.append(desired_name+EXTENSION) 104 | self.statusBar.showMessage("Creating notebook", 2000) 105 | self.accept() 106 | 107 | 108 | class NewFolder(Dialog): 109 | 110 | def __init__(self, parent=None): 111 | Dialog.__init__(self, parent) 112 | self.names = [elem for elem in self.info.folders] 113 | self.initUI() 114 | 115 | def initUI(self): 116 | self.log.info("Creating a 'New Folder' form") 117 | self.setWindowTitle("New folder") 118 | 119 | # Define the field: 120 | # Name 121 | formLayout = QtGui.QFormLayout() 122 | self.nameLineEdit = QtGui.QLineEdit() 123 | 124 | formLayout.addRow(self.translate("Folder's &name:"), self.nameLineEdit) 125 | self.layout().addLayout(formLayout) 126 | 127 | buttonLayout = QtGui.QHBoxLayout() 128 | 129 | # Add the "Create" button, as a confirmation, and the "Cancel" one 130 | self.createButton = QtGui.QPushButton("&Create") 131 | self.createButton.clicked.connect(self.createFolder) 132 | self.cancelButton = QtGui.QPushButton("C&ancel") 133 | self.cancelButton.clicked.connect(self.clean_reject) 134 | 135 | buttonLayout.addStretch() 136 | buttonLayout.addWidget(self.createButton) 137 | buttonLayout.addWidget(self.cancelButton) 138 | self.layout().addLayout(buttonLayout) 139 | 140 | # Create a status bar 141 | self.statusBar = QtGui.QStatusBar() 142 | self.layout().addWidget(self.statusBar) 143 | 144 | def createFolder(self): 145 | desired_name = str(self.nameLineEdit.text()) 146 | self.log.info("Desired Folder name: "+desired_name) 147 | if not desired_name or len(desired_name) < 2: 148 | self.statusBar.showMessage("name too short", 2000) 149 | self.log.info("name rejected: too short") 150 | else: 151 | if desired_name in self.names: 152 | self.statusBar.showMessage("name already used", 2000) 153 | self.log.info("name rejected, already used") 154 | else: 155 | # Actually creating the folder 156 | self.info.folders.append(desired_name) 157 | self.statusBar.showMessage("Creating Folder", 2000) 158 | self.accept() 159 | 160 | 161 | class NewEntry(Dialog): 162 | """Create a new entry in the notebook""" 163 | 164 | def __init__(self, parent=None): 165 | Dialog.__init__(self, parent) 166 | self.initUI() 167 | 168 | def initUI(self): 169 | self.log.info("Creating a 'New Entry' form") 170 | 171 | self.setWindowTitle("New entry") 172 | 173 | # Define the fields: Name, tags and body 174 | titleLineLayout = QtGui.QHBoxLayout() 175 | self.titleLineLabel = QtGui.QLabel("Title:") 176 | self.titleLineLabel.setFixedWidth(40) 177 | self.titleLineEdit = QtGui.QLineEdit() 178 | titleLineLayout.addWidget(self.titleLineLabel) 179 | titleLineLayout.addWidget(self.titleLineEdit) 180 | 181 | # create TagCompletion with tags from current file 182 | index = self.parent.tabs.currentIndex() 183 | notebook = os.path.join(self.info.level, self.info.notebooks[index]) 184 | self.log.info("reading tags from %s" % notebook) 185 | _, tags = tp.from_notes_to_markdown(notebook) 186 | tagsLineLayout = QtGui.QHBoxLayout() 187 | self.tagsLineLabel = QtGui.QLabel("Tags:") 188 | self.tagsLineLabel.setFixedWidth(40) 189 | self.tagsLineEdit = TagCompletion(tags) 190 | tagsLineLayout.addWidget(self.tagsLineLabel) 191 | tagsLineLayout.addWidget(self.tagsLineEdit) 192 | 193 | corpusBoxLayout = QtGui.QHBoxLayout() 194 | self.corpusBoxLabel = QtGui.QLabel("Body:") 195 | self.corpusBoxLabel.setFixedWidth(40) 196 | self.corpusBoxLabel.setAlignment( 197 | QtCore.Qt.AlignTop) 198 | self.corpusBox = QtGui.QTextEdit() 199 | corpusBoxLayout.addWidget(self.corpusBoxLabel) 200 | corpusBoxLayout.addWidget(self.corpusBox) 201 | 202 | self.layout().addLayout(titleLineLayout) 203 | self.layout().addLayout(tagsLineLayout) 204 | self.layout().addLayout(corpusBoxLayout) 205 | 206 | # Define the RHS with Ok, Cancel and list of tags TODO) 207 | buttonLayout = QtGui.QHBoxLayout() 208 | 209 | self.newImageButton = QtGui.QPushButton("Insert &Image") 210 | self.newImageButton.clicked.connect(self.insertImage) 211 | 212 | self.okButton = QtGui.QPushButton("&Ok") 213 | self.okButton.clicked.connect(self.creating_entry) 214 | acceptShortcut = QtGui.QShortcut( 215 | QtGui.QKeySequence(self.translate("Shift+Enter")), self.corpusBox) 216 | acceptShortcut.activated.connect(self.creating_entry) 217 | 218 | self.cancelButton = QtGui.QPushButton("&Cancel") 219 | self.cancelButton.clicked.connect(self.clean_reject) 220 | 221 | # Add a spacer before so that the buttons do not stretch 222 | buttonLayout.addStretch() 223 | buttonLayout.addWidget(self.newImageButton) 224 | buttonLayout.addWidget(self.okButton) 225 | buttonLayout.addWidget(self.cancelButton) 226 | 227 | self.layout().addLayout(buttonLayout) 228 | # Create the status bar 229 | self.statusBar = QtGui.QStatusBar(self) 230 | self.layout().addWidget(self.statusBar) 231 | self.titleLineEdit.setFocus() 232 | 233 | def creating_entry(self): 234 | # Check if title is valid (non-empty) 235 | title = str(self.titleLineEdit.text()) 236 | if not title or len(title) < 2: 237 | self.statusBar.showMessage(self.translate("Invalid title"), 2000) 238 | return 239 | tags = str(self.tagsLineEdit.getTextWithNormalizedSeparators()) 240 | if not tags or len(tags) < 2: 241 | self.statusBar.showMessage(self.translate("Invalid tags"), 2000) 242 | return 243 | tags = [tag.strip() for tag in tags.split(',')] 244 | corpus = self.corpusBox.toPlainText() 245 | if not corpus or len(corpus) < 2: 246 | self.statusBar.showMessage(self.translate("Empty entry"), 2000) 247 | return 248 | # Storing the variables to be recovered afterwards 249 | self.title = title 250 | self.tags = tags 251 | self.corpus = corpus 252 | self.clean_accept() 253 | 254 | def insertImage(self): 255 | """ insert an image path as markdown at the current cursor position """ 256 | self.popup = QtGui.QFileDialog() 257 | filename = self.popup.getOpenFileName(self, 258 | "select an image", 259 | "", 260 | "Image Files (*.png *.jpg *.bmp *.jpeg *.svg *.gif)" + \ 261 | ";;all files (*.*)") 262 | 263 | # QFileDialog returns a tuple with filename and used filter 264 | if filename[0]: 265 | imagemarkdown = tp.create_image_markdown(filename[0]) 266 | self.corpusBox.insertPlainText(imagemarkdown) 267 | 268 | 269 | class SetExternalEditor(Dialog): 270 | 271 | """popup for setting the commandline for the external editor""" 272 | 273 | def __init__(self, parent=None): 274 | Dialog.__init__(self, parent) 275 | self.initUI() 276 | 277 | def initUI(self): 278 | self.log.info("Creating a 'Set External Editor' form") 279 | 280 | self.setWindowTitle("Set External Editor") 281 | 282 | # Define the main window horizontal layout 283 | hboxLayout = QtGui.QHBoxLayout() 284 | 285 | # Define the field 286 | formLayout = QtGui.QFormLayout() 287 | self.commandlineEdit = QtGui.QLineEdit() 288 | 289 | self.commandlineEdit.setText(self.info.externalEditor) 290 | formLayout.addRow(self.tr("&external editor:"), self.commandlineEdit) 291 | 292 | hboxLayout.addLayout(formLayout) 293 | 294 | # Define the RHS with Ok, Cancel and list of tags TODO) 295 | buttonLayout = QtGui.QVBoxLayout() 296 | 297 | self.okButton = QtGui.QPushButton("&Ok") 298 | self.okButton.clicked.connect(self.set_commandline) 299 | 300 | self.cancelButton = QtGui.QPushButton("&Cancel") 301 | self.cancelButton.clicked.connect(self.clean_reject) 302 | 303 | buttonLayout.addStretch() 304 | buttonLayout.addWidget(self.okButton) 305 | buttonLayout.addWidget(self.cancelButton) 306 | 307 | hboxLayout.addLayout(buttonLayout) 308 | # Create the status bar 309 | self.statusBar = QtGui.QStatusBar(self) 310 | # Create a permanent widget displaying what we are doing 311 | statusWidget = \ 312 | QtGui.QLabel("setting the commandline for the external editor") 313 | self.statusBar.addPermanentWidget(statusWidget) 314 | 315 | self.layout().addLayout(hboxLayout) 316 | self.layout().addWidget(self.statusBar) 317 | 318 | def set_commandline(self): 319 | """check the commandline write it to the settings and return""" 320 | # Check if text is a valid commandline 321 | commandline = str(self.commandlineEdit.text()) 322 | if not commandline or len(commandline) < 2: 323 | self.statusBar.showMessage(self.tr("Invalid Commandline"), 2000) 324 | return 325 | # Storing the variables to be recovered afterwards 326 | self.commandline = commandline 327 | self.settings = QtCore.QSettings("audren", "NoteOrganiser") 328 | self.settings.setValue("externalEditor", self.commandline) 329 | self.clean_accept() 330 | -------------------------------------------------------------------------------- /noteorganiser/NoteOrganiser.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. module:: NoteOrganiser 3 | :synopsis: Define the NoteOrganiser class 4 | 5 | .. moduleauthor:: Benjamin Audren 6 | """ 7 | from __future__ import unicode_literals 8 | # Main imports 9 | import sys 10 | import os 11 | import re 12 | from PySide import QtGui 13 | from PySide import QtCore 14 | 15 | # Local imports 16 | from noteorganiser.popups import SetExternalEditor 17 | from noteorganiser.frames import Library, Editing, Preview 18 | from noteorganiser.logger import create_logger 19 | import noteorganiser.configuration as conf 20 | 21 | 22 | class NoteOrganiser(QtGui.QMainWindow): 23 | """ 24 | Main Program 25 | 26 | The main window will consist of three tabs, to help you navigate your 27 | notes: Library, editing and previewing. 28 | """ 29 | states = [ 30 | 'library', # The starting one, displaying the notebooks 31 | 'editing', 32 | 'preview'] 33 | 34 | def __init__(self): 35 | QtGui.QMainWindow.__init__(self) 36 | 37 | # Define a logger 38 | log_path = os.path.join( 39 | os.path.expanduser("~"), '.noteorganiser', 'log') 40 | logger = create_logger('INFO', 'file', log_path) 41 | # Recover the folder path and the notebooks 42 | root, notebooks, folders = conf.initialise(logger) 43 | 44 | # Create an instance of the Information class to store all this. 45 | info = conf.Information(logger, root, notebooks, folders) 46 | 47 | # Store reference to the info class 48 | self.info = info 49 | # Shortcut for the logger 50 | self.log = self.info.logger 51 | 52 | self.initUI() 53 | self.initLogic() 54 | self.show() 55 | 56 | # Show added the OS title bar, modifying the height of the window. It 57 | # is substracted below such that by default, if no previous 58 | # configuration was found, the window occupies the whole height. 59 | self.settings = QtCore.QSettings("audren", "NoteOrganiser") 60 | if self.settings.value("geometry"): 61 | self.restoreGeometry(self.settings.value("geometry")) 62 | else: 63 | # This function returns the available dimension excluding the 64 | # taskbar. 65 | geometry = QtGui.QApplication.desktop().availableGeometry() 66 | # We still have to remove the added height of OS title bar 67 | extra_height = self.frameGeometry().height() - self.geometry().height() 68 | extra_width = self.frameGeometry().width() - self.geometry().width() 69 | self.setGeometry( 70 | # TODO the 3./4 is a hack for windows (adds more space to the 71 | # top, probably not robust) 72 | extra_width/2, extra_height*3./4, 73 | (geometry.width()-extra_width)/2., 74 | geometry.height()-extra_height) 75 | 76 | def initUI(self): 77 | """Initialise all the User Interface""" 78 | self.log.info("Starting UI init of %s" % self.__class__.__name__) 79 | self.initWidgets() 80 | self.initMenuBar() 81 | self.initStatusBar() 82 | self.log.info("Finished UI init of %s" % self.__class__.__name__) 83 | 84 | # set the window-icon 85 | path = os.path.abspath(os.path.dirname(__file__)) 86 | self.setWindowIcon(QtGui.QIcon( 87 | os.path.join(path, 'assets', 'notebook-128.png'))) 88 | 89 | self.setWindowTitle('Note Organiser') 90 | 91 | def initMenuBar(self): 92 | """Defining the menu bar""" 93 | self.log.info("Creating Menu Bar") 94 | # Exit 95 | exitAction = QtGui.QAction('&Exit', self) 96 | exitAction.setShortcut('Ctrl+Q') 97 | exitAction.setStatusTip('Exit application') 98 | exitAction.triggered.connect(self.cleanClose) 99 | 100 | # Toggle displaying empty folders 101 | toggleEmptyAction = QtGui.QAction('display empty folders', self) 102 | toggleEmptyAction.setStatusTip('Toggle the display of empty folders') 103 | toggleEmptyAction.setCheckable(True) 104 | toggleEmptyAction.setChecked(self.info.display_empty) 105 | toggleEmptyAction.triggered.connect( 106 | self.library.shelves.toggleDisplayEmpty) 107 | 108 | # Toggle refreshing of editor-page when file changes 109 | toggleRefreshAction = QtGui.QAction('automatically refresh editor', 110 | self) 111 | toggleRefreshAction.setStatusTip( 112 | 'automatically refresh editor when the file changes') 113 | toggleRefreshAction.setCheckable(True) 114 | toggleRefreshAction.setChecked(self.info.refreshEditor) 115 | toggleRefreshAction.triggered.connect( 116 | self.toggleRefresh) 117 | 118 | # show popup for external editor commandline 119 | externalEditor = QtGui.QAction('set external Editor', self) 120 | externalEditor.setStatusTip( 121 | 'Set the Commandline for the external Editor') 122 | externalEditor.triggered.connect(self.setExternalEditor) 123 | 124 | # Toggle use of Table of Content 125 | toggleUseTOC = QtGui.QAction('use TOC in output', self) 126 | toggleUseTOC.setStatusTip( 127 | 'Toggle the usage of Table of Content in HTML-output') 128 | toggleUseTOC.setCheckable(True) 129 | toggleUseTOC.setChecked(self.info.use_TOC) 130 | toggleUseTOC.triggered.connect(self.toggleUseTOC) 131 | 132 | # Choose the main folder 133 | mainFolderAction = QtGui.QAction('change the main directory', self) 134 | mainFolderAction.setStatusTip( 135 | 'Select another folder as the root level for the notebooks') 136 | mainFolderAction.triggered.connect(self.chooseMainFolder) 137 | 138 | # Zoom-in 139 | zoomInAction = QtGui.QAction('Zoom-in', self) 140 | zoomInAction.setShortcut('Ctrl++') 141 | zoomInAction.setStatusTip('Zoom in') 142 | zoomInAction.triggered.connect(self.zoomIn) 143 | 144 | # Zoom-out 145 | zoomOutAction = QtGui.QAction('Zoom-out', self) 146 | zoomOutAction.setShortcut('Ctrl+-') 147 | zoomOutAction.setStatusTip('Zoom out') 148 | zoomOutAction.triggered.connect(self.zoomOut) 149 | 150 | # Reset Size 151 | resetSizeAction = QtGui.QAction('Reset-size', self) 152 | resetSizeAction.setShortcut('Ctrl+0') 153 | resetSizeAction.setStatusTip('Reset size') 154 | resetSizeAction.triggered.connect(self.resetSize) 155 | 156 | # Create the menu 157 | menubar = self.menuBar() 158 | # File menu 159 | fileMenu = menubar.addMenu('&File') 160 | fileMenu.addAction(exitAction) 161 | 162 | # Options menu 163 | optionsMenu = menubar.addMenu('&Options') 164 | optionsMenu.addAction(toggleEmptyAction) 165 | optionsMenu.addAction(toggleRefreshAction) 166 | optionsMenu.addAction(externalEditor) 167 | optionsMenu.addAction(toggleUseTOC) 168 | optionsMenu.addAction(mainFolderAction) 169 | 170 | # Display menu 171 | displayMenu = menubar.addMenu('&Display') 172 | displayMenu.addAction(zoomInAction) 173 | displayMenu.addAction(zoomOutAction) 174 | displayMenu.addAction(resetSizeAction) 175 | 176 | def setExternalEditor(self): 177 | """set the variable for the external editor""" 178 | self.popup = SetExternalEditor(self) 179 | #this will show the popup 180 | ok = self.popup.exec_() 181 | # the return code is True if successfull 182 | if ok: 183 | #Recover the field 184 | self.info.externalEditor = self.popup.commandline 185 | 186 | def toggleRefresh(self): 187 | """ 188 | toggle if the editor gets refreshed automatically when the file 189 | changes 190 | """ 191 | #save settings 192 | self.info.refreshEditor = not self.info.refreshEditor 193 | self.settings = QtCore.QSettings("audren", "NoteOrganiser") 194 | self.settings.setValue("refreshEditor", self.info.refreshEditor) 195 | if self.info.refreshEditor: 196 | self.log.info('auto refresh enabled') 197 | else: 198 | self.log.info('auto refresh disabled') 199 | 200 | def toggleUseTOC(self): 201 | """toggle the use of the Table of Content in html-output""" 202 | self.info.use_TOC = not self.info.use_TOC 203 | #save the setting 204 | self.settings = QtCore.QSettings("audren", "NoteOrganiser") 205 | self.settings.setValue("use_TOC", self.info.use_TOC) 206 | self.preview.reload() 207 | 208 | def chooseMainFolder(self): 209 | """Select another folder for the source of notebooks""" 210 | # Recover the folder path and the notebooks 211 | root, notebooks, folders = conf.initialise( 212 | self.log, force_folder_change=True) 213 | 214 | # Create an instance of the Information class to store all this. 215 | self.info.root = root 216 | self.info.level = root 217 | self.info.notebooks = notebooks 218 | self.info.folders = folders 219 | 220 | # Refresh the display of the current widget 221 | self.tabs.currentWidget().refresh() 222 | 223 | def initStatusBar(self): 224 | """Defining the status bar""" 225 | self.log.info("Creating Status Bar") 226 | self.statusBar() 227 | 228 | def initWidgets(self): 229 | """Creating the tabbed widget containing the three main tabs""" 230 | # Creating the tabbed widget 231 | self.tabs = QtGui.QTabWidget(self) 232 | 233 | # Creating the three tabs. Through their parent, they will recover the 234 | # reference to the list of notebooks. 235 | self.library = Library(self) 236 | self.editing = Editing(self) 237 | self.preview = Preview(self) 238 | 239 | # Adding them to the tabs widget 240 | self.tabs.addTab(self.library, "&Library") 241 | self.tabs.addTab(self.editing, "&Editing") 242 | self.tabs.addTab(self.preview, "Previe&w") 243 | 244 | # adding additional shortcuts 245 | self.libraryShortcut = QtGui.QAction('library', self) 246 | self.libraryShortcut.setShortcut('Ctrl+L') 247 | self.libraryShortcut.triggered.connect(self.setActiveTab) 248 | self.addAction(self.libraryShortcut) 249 | self.editingShortcut = QtGui.QAction('editing', self) 250 | self.editingShortcut.setShortcut('Ctrl+E') 251 | self.editingShortcut.triggered.connect(self.setActiveTab) 252 | self.addAction(self.editingShortcut) 253 | self.previewShortcut = QtGui.QAction('preview', self) 254 | self.previewShortcut.setShortcut('Ctrl+W') 255 | self.previewShortcut.triggered.connect(self.setActiveTab) 256 | self.addAction(self.previewShortcut) 257 | 258 | # Set the tabs widget to be the center widget of the main window 259 | self.log.info("Setting the central widget") 260 | self.setCentralWidget(self.tabs) 261 | 262 | # show only the toolbar for the active tab 263 | self.tabs.currentChanged.connect(self.showActiveToolBar) 264 | 265 | def initLogic(self): 266 | """Linking signals from widgets to functions""" 267 | self.state = 'library' 268 | # Connect slots to signal 269 | # * shelves refresh to the editing refresh 270 | self.library.shelves.refreshSignal.connect(self.editing.refresh) 271 | # * shelves switchTab to the own switchTab method 272 | self.library.shelves.switchTabSignal.connect(self.switchTab) 273 | # * shelves preview signal to previewNotebook 274 | self.library.shelves.previewSignal.connect(self.previewNotebook) 275 | # * editing preview to preview loadNotebook, and switch the tab 276 | self.editing.loadNotebook.connect(self.previewNotebook) 277 | 278 | @QtCore.Slot(str, str) 279 | def switchTab(self, tab, notebook): 280 | """Switch Tab to the desired target""" 281 | self.tabs.setCurrentIndex(self.states.index(tab)) 282 | if tab == 'editing': 283 | self.editing.switchNotebook(notebook) 284 | 285 | @QtCore.Slot(str) 286 | def previewNotebook(self, notebook): 287 | """Preview the desired notebook""" 288 | self.editing.switchNotebook( 289 | os.path.splitext(os.path.basename(notebook))[0]) 290 | if self.preview.loadNotebook(notebook): 291 | self.switchTab('preview', notebook) 292 | 293 | def closeEvent(self, _): 294 | self.cleanClose() 295 | 296 | def cleanClose(self): 297 | """Overload the closeEvent to store the geometry""" 298 | self.settings = QtCore.QSettings("audren", "NoteOrganiser") 299 | self.settings.setValue("geometry", self.saveGeometry()) 300 | self.close() 301 | 302 | def zoomIn(self): 303 | """send a zoom-in signal to the current tab""" 304 | self.tabs.currentWidget().zoomIn() 305 | 306 | def zoomOut(self): 307 | """send a zoom-out signal to the current tab""" 308 | self.tabs.currentWidget().zoomOut() 309 | 310 | def resetSize(self): 311 | self.tabs.currentWidget().resetSize() 312 | 313 | @QtCore.Slot(int) 314 | def showActiveToolBar(self, tabIndex): 315 | """show only the toolbar for the active tab""" 316 | getattr(self, self.states[tabIndex]).toolbar.setVisible(True) 317 | for index in range(len(self.states)): 318 | if index != tabIndex: 319 | getattr(self, 320 | self.states[index]).toolbar.setVisible(False) 321 | 322 | def setActiveTab(self): 323 | """ 324 | set the active tab in the tabWidget to the widget for which the 325 | shortcut was used 326 | """ 327 | self.tabs.setCurrentWidget(getattr(self, self.sender().iconText())) 328 | 329 | 330 | def main(args): 331 | """Create the application, and execute it""" 332 | # Initialise the main Qt application 333 | application = QtGui.QApplication(args) 334 | 335 | # Define the main window 336 | NoteOrganiser() 337 | 338 | # Run 339 | sys.exit(application.exec_()) 340 | 341 | 342 | if __name__ == "__main__": 343 | main(sys.argv) 344 | -------------------------------------------------------------------------------- /noteorganiser/tests/test_frames.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import datetime 4 | from PySide import QtGui 5 | from PySide import QtCore 6 | import pytest 7 | 8 | # Frames to test 9 | from ..frames import CustomFrame 10 | from ..frames import Library 11 | from ..frames import Shelves 12 | from ..frames import TextEditor 13 | from ..frames import Editing 14 | from ..frames import Preview 15 | from .custom_fixtures import parent 16 | 17 | from ..widgets import PicButton 18 | from ..constants import EXTENSION 19 | 20 | 21 | def test_custom_frame(qtbot, parent): 22 | # Creating a basic frame. This should raise a NotImplementedError 23 | with pytest.raises(NotImplementedError): 24 | CustomFrame(parent) 25 | 26 | # Define a daughter class which implements a dummy initUI method. Then, 27 | # calling zoomIn, zoomOut or resetSize should also raise a 28 | # NotImplementedError 29 | class Dummy(CustomFrame): 30 | def initUI(self): 31 | pass 32 | 33 | dummy = Dummy(parent) 34 | qtbot.addWidget(dummy) 35 | with pytest.raises(NotImplementedError): 36 | dummy.zoomIn() 37 | with pytest.raises(NotImplementedError): 38 | dummy.zoomOut() 39 | with pytest.raises(NotImplementedError): 40 | dummy.resetSize() 41 | with pytest.raises(NotImplementedError): 42 | dummy.initToolBar() 43 | 44 | 45 | def test_library(qtbot, parent): 46 | # Creating the object and adding it to the bot 47 | library = Library(parent) 48 | qtbot.addWidget(library) 49 | 50 | # Check the nature of the shelves object 51 | assert hasattr(library, 'shelves') 52 | assert isinstance(library.shelves, Shelves) 53 | 54 | # Check that the refresh method of the library at least call the refresh 55 | # method of the shelves, and therefore, emits the expected signal 56 | with qtbot.waitSignal(library.shelves.refreshSignal, timeout=1000) as \ 57 | refresh: 58 | library.refresh() 59 | assert refresh.signal_triggered, \ 60 | "refreshing the library should transmit the signal to the shelves" 61 | 62 | 63 | def test_shelves(qtbot, parent, mocker): 64 | # Creating the shelves, and adding them to the bot 65 | library = Library(parent) 66 | shelves = library.shelves 67 | qtbot.addWidget(shelves) 68 | 69 | # Checking if the buttons list was created, and that it contains only two 70 | # elements (the notebook, and the folder) 71 | assert hasattr(shelves, 'buttons'), 'buttons attribute not created' 72 | assert len(shelves.buttons) == 3, 'not all buttons were created' 73 | for button in shelves.buttons: 74 | assert isinstance(button, PicButton) 75 | # Asserting that left clicking on the notebook icon (first element of the 76 | # buttons attribute) sends the proper signal. 77 | with qtbot.waitSignal(shelves.switchTabSignal, timeout=1000) as switch: 78 | qtbot.mouseClick(shelves.buttons[0], QtCore.Qt.LeftButton) 79 | assert switch.signal_triggered, \ 80 | "clicking on a notebook should trigger a switchTabSignal" 81 | 82 | # Checking that the up button is currently inaccessible (we are still in 83 | # the root folder 84 | assert not library.toolbar.widgetForAction(library.upAction).isEnabled(), \ 85 | "upButton should be disabled while in root" 86 | 87 | # Checking the behaviour when clicking on a folder 88 | with qtbot.waitSignal(shelves.refreshSignal, timeout=1000) as way_down: 89 | qtbot.mouseClick(shelves.buttons[2], QtCore.Qt.LeftButton) 90 | # Check that the info.level has changed properly 91 | assert shelves.info.level != shelves.info.root, "did not change dir" 92 | # Check that now, the upButton is enabled 93 | assert library.toolbar.widgetForAction(library.upAction).isEnabled(), \ 94 | "upButton was not enabled" 95 | assert way_down.signal_triggered, "going down did not send a refreshSignal" 96 | 97 | # Go back to the root 98 | with qtbot.waitSignal(shelves.refreshSignal, timeout=1000) as way_up: 99 | #qtbot.mouseClick(shelves.upButton, QtCore.Qt.LeftButton) 100 | qtbot.mouseClick(library.toolbar.widgetForAction(library.upAction), 101 | QtCore.Qt.LeftButton) 102 | assert shelves.info.level == shelves.info.root, \ 103 | "the upButton did not go back up" 104 | #TODO with toolbar items 105 | assert not \ 106 | library.toolbar.widgetForAction(library.upAction).isEnabled(), \ 107 | "the upButton was not properly reconnected" 108 | assert way_up.signal_triggered, "going up did not send a refreshSignal" 109 | 110 | # Test right click on the notebook. It should **not** trigger a switch tab 111 | with qtbot.waitSignal(shelves.refreshSignal, timeout=100) as right: 112 | qtbot.mouseClick(shelves.buttons[0], QtCore.Qt.RightButton) 113 | assert not right.signal_triggered 114 | 115 | # Test right click, should open the menu TODO 116 | # Test clicking on the menu, should actually delete the file, and send a 117 | # refresh signal. TODO. temporary fix: call directly removeNotebook method 118 | # Mock the question QMessageBox 119 | question = mocker.patch.object(QtGui.QMessageBox, 'question', 120 | return_value=QtGui.QMessageBox.No) 121 | shelves.removeNotebook('example') 122 | # Check that nothing happened 123 | assert len(shelves.buttons) == 3, \ 124 | "Saying no to the question did not stop the removal" 125 | 126 | with qtbot.waitSignal(shelves.refreshSignal, timeout=2000) as remove: 127 | # Reuse the question object because of a Pyside bug under Python 3.3 128 | question.return_value = QtGui.QMessageBox.Yes 129 | shelves.removeNotebook('example') 130 | assert remove.signal_triggered 131 | # Check that the file was indeed removed 132 | assert len(shelves.buttons) == 2, \ 133 | "Saying yes to the question did not remove the notebook" 134 | 135 | # Adding a notebook 136 | def interact_newN(): 137 | # Create a notebook called toto 138 | qtbot.keyClicks(shelves.popup.nameLineEdit, 'toto') 139 | qtbot.mouseClick(shelves.popup.createButton, QtCore.Qt.LeftButton) 140 | 141 | with qtbot.waitSignal(shelves.refreshSignal, timeout=2000) as newN: 142 | # Create a timer, to let the window be created, then fill in the 143 | # information 144 | QtCore.QTimer.singleShot(200, interact_newN) 145 | qtbot.mouseClick( 146 | library.toolbar.widgetForAction(library.newNotebookAction), 147 | QtCore.Qt.LeftButton) 148 | 149 | assert len(shelves.buttons) == 3, "the notebook was not created" 150 | assert shelves.info.notebooks[-1] == 'toto'+EXTENSION, \ 151 | "the notebook was not added to the information instance" 152 | assert newN.signal_triggered 153 | 154 | # Trying to add a notebook with an existing name 155 | def interact_existingN(): 156 | # Create a notebook called toto 157 | qtbot.keyClicks(shelves.popup.nameLineEdit, 'toto') 158 | qtbot.mouseClick(shelves.popup.createButton, QtCore.Qt.LeftButton) 159 | qtbot.mouseClick(shelves.popup.cancelButton, QtCore.Qt.LeftButton) 160 | 161 | # Trying to add a notebook with the same name 162 | with qtbot.waitSignal(shelves.refreshSignal, timeout=100) as existingN: 163 | QtCore.QTimer.singleShot(200, interact_existingN) 164 | qtbot.mouseClick( 165 | library.toolbar.widgetForAction(library.newNotebookAction), 166 | QtCore.Qt.LeftButton) 167 | 168 | assert len(shelves.buttons) == 3, "Same names are not checked" 169 | assert not existingN.signal_triggered 170 | 171 | # Trying to add a notebook with a name too short 172 | def interact_tooShortN(): 173 | # Create a notebook called toto 174 | qtbot.keyClicks(shelves.popup.nameLineEdit, 't') 175 | qtbot.mouseClick(shelves.popup.createButton, QtCore.Qt.LeftButton) 176 | qtbot.mouseClick(shelves.popup.cancelButton, QtCore.Qt.LeftButton) 177 | 178 | # Trying to add a notebook with a too short name 179 | with qtbot.waitSignal(shelves.refreshSignal, timeout=100) as tooShortN: 180 | QtCore.QTimer.singleShot(200, interact_tooShortN) 181 | qtbot.mouseClick( 182 | library.toolbar.widgetForAction(library.newNotebookAction), 183 | QtCore.Qt.LeftButton) 184 | 185 | assert len(shelves.buttons) == 3, "Too short names are not checked" 186 | assert not tooShortN.signal_triggered 187 | 188 | # Adding a folder 189 | def interact_newF(): 190 | qtbot.keyClicks(shelves.popup.nameLineEdit, 'titi') 191 | qtbot.mouseClick(shelves.popup.createButton, QtCore.Qt.LeftButton) 192 | 193 | with qtbot.waitSignal(shelves.refreshSignal, timeout=1000) as newF: 194 | QtCore.QTimer.singleShot(200, interact_newF) 195 | qtbot.mouseClick( 196 | library.toolbar.widgetForAction(library.newFolderAction), 197 | QtCore.Qt.LeftButton) 198 | assert len(shelves.buttons) == 0, \ 199 | "the folder was not created, or the level was not changed" 200 | # Create a notebook called toto inside the titi folder 201 | QtCore.QTimer.singleShot(200, interact_newN) 202 | qtbot.mouseClick( 203 | library.toolbar.widgetForAction(library.newNotebookAction), 204 | QtCore.Qt.LeftButton) 205 | 206 | assert len(shelves.buttons) == 1, "the notebook was not created" 207 | assert shelves.info.notebooks == ['toto'+EXTENSION], \ 208 | "the notebook was not added to the information instance" 209 | 210 | assert newF.signal_triggered 211 | 212 | # Go back, and check that creating a new folder that already exists does 213 | # not overwrite it 214 | qtbot.mouseClick(library.toolbar.widgetForAction(library.upAction), 215 | QtCore.Qt.LeftButton) 216 | 217 | def interact_existingF(): 218 | qtbot.keyClicks(shelves.popup.nameLineEdit, 'toto') 219 | qtbot.mouseClick(shelves.popup.createButton, QtCore.Qt.LeftButton) 220 | 221 | with qtbot.waitSignal(shelves.refreshSignal, timeout=1000) as existingF: 222 | QtCore.QTimer.singleShot(200, interact_existingF) 223 | qtbot.mouseClick( 224 | library.toolbar.widgetForAction(library.newFolderAction), 225 | QtCore.Qt.LeftButton) 226 | assert len(shelves.buttons) == 1, \ 227 | "the existing folder was overwritten" 228 | assert existingF.signal_triggered 229 | 230 | # Create an empty file, and remove it TODO 231 | 232 | # test preview function on shift click 233 | # this should show the clicked notebook in the preview-tab 234 | new_index = 0 235 | with qtbot.waitSignal(shelves.refreshSignal, timeout=100) as right: 236 | with qtbot.waitSignal(shelves.previewSignal, timeout=1000) as switch: 237 | qtbot.mouseClick(shelves.buttons[new_index], QtCore.Qt.LeftButton, 238 | QtCore.Qt.ShiftModifier) 239 | assert switch.signal_triggered, \ 240 | "shift-clicking on a notebook should trigger a previewSignal" 241 | assert not right.signal_triggered, \ 242 | "shift-clicking should NOT trigger a refreshSignal" 243 | 244 | 245 | def test_text_editor(qtbot, parent): 246 | editing = Editing(parent) 247 | text_editor = editing.tabs.currentWidget() 248 | qtbot.addWidget(text_editor) 249 | 250 | # Load the file 251 | source = os.path.join(parent.info.level, parent.info.notebooks[0]) 252 | text_editor.setSource(source) 253 | 254 | # check that now the text is non-empty 255 | assert text_editor.text.toPlainText() is not None, \ 256 | "The source file was not read" 257 | 258 | def check_final_line(text, check_new_line=True): 259 | if check_new_line: 260 | start = -len(text)-1 261 | new_text = text_editor.text.toPlainText()[start:] 262 | control = "\n"+text 263 | else: 264 | start = -len(text) 265 | new_text = text_editor.text.toPlainText()[start:] 266 | control = text 267 | assert new_text == control, "The line was not added properly" 268 | 269 | # append a line with the method appendText used in NewEntry 270 | text_editor.appendText("Life is beautiful") 271 | check_final_line("Life is beautiful") 272 | 273 | # Check that it has been saved while appending by reloading 274 | text_editor.setSource(source) 275 | check_final_line("Life is beautiful") 276 | 277 | # Try to write in the end without saving, and reloading 278 | # TODO how to input a "enter" pressed, ie, carriage return? 279 | qtbot.keyClicks(text_editor.text, 'Life is beautiful') 280 | check_final_line("Life is beautiful", check_new_line=False) 281 | 282 | # Reload from disk, and check that the last line is gone (i.e., that there 283 | # is still the "\nLife is beautiful", and not "\nLife is BeautifulLife is 284 | # beautiful" 285 | qtbot.mouseClick(editing.toolbar.widgetForAction(editing.readAction), 286 | QtCore.Qt.LeftButton) 287 | check_final_line("Life is beautiful") 288 | 289 | # Try to zoom out, in, and reset, simply testing that the font has been 290 | # updated, the text has the proper font, and that the pointSize has the 291 | # right value 292 | def check_font_size(size): 293 | font = text_editor.font 294 | text_font = text_editor.text.font() 295 | # Check that the font was set to the right value 296 | assert font.pointSize() == size 297 | # Check that the text was properly updated 298 | assert font.pointSize() == text_font.pointSize() 299 | 300 | text_editor.zoomIn() 301 | check_font_size(text_editor.defaultFontSize+1) 302 | 303 | text_editor.zoomOut() 304 | text_editor.zoomOut() 305 | check_font_size(text_editor.defaultFontSize-1) 306 | 307 | text_editor.resetSize() 308 | check_font_size(text_editor.defaultFontSize) 309 | 310 | 311 | def test_editing(qtbot, parent, mocker): 312 | """Test the editing tab""" 313 | editing = Editing(parent) 314 | qtbot.addWidget(editing) 315 | 316 | # Check that there is only one tab 317 | assert editing.tabs.count() == 2, "the tabs were not created properly" 318 | 319 | # Test the new entry button 320 | def interact_newEntry(): 321 | # Create a post called toto, with tags tata and tutu and entry titi 322 | qtbot.keyClicks(editing.popup.titleLineEdit, 'toto') 323 | qtbot.keyClicks(editing.popup.tagsLineEdit, 'tata, tutu') 324 | qtbot.keyClicks(editing.popup.corpusBox, 'titi') 325 | qtbot.mouseClick(editing.popup.okButton, QtCore.Qt.LeftButton) 326 | 327 | QtCore.QTimer.singleShot(200, interact_newEntry) 328 | qtbot.mouseClick(editing.toolbar.widgetForAction(editing.newEntryAction), 329 | QtCore.Qt.LeftButton) 330 | 331 | # Check that the entry was appended, with the date properly set 332 | new_text = editing.tabs.currentWidget().text.toPlainText()[ 333 | -44:] 334 | expectation = '\ntoto\n----\n# tata, tutu\n\n*%s*\n\ntiti\n' % ( 335 | datetime.date.today().strftime("%d/%m/%Y")) 336 | assert new_text == expectation, "The new entry was not appended %s, %s" % ( 337 | new_text, expectation) 338 | 339 | # Check if preview button works by sending the signal loadNotebook 340 | with qtbot.waitSignal(editing.loadNotebook, timeout=1000) as preview: 341 | qtbot.mouseClick( 342 | editing.toolbar.widgetForAction(editing.previewAction), 343 | QtCore.Qt.LeftButton) 344 | 345 | assert preview.signal_triggered, \ 346 | "asking for previewing does not send the right signal" 347 | 348 | 349 | # Insert Image 350 | before_cancel_text = editing.tabs.currentWidget().text.toPlainText() 351 | 352 | file_dialog = mocker.patch.object(QtGui.QFileDialog, 'getOpenFileName', 353 | return_value=('', '')) 354 | qtbot.mouseClick(editing.toolbar.widgetForAction(editing.imageInsertAction), 355 | QtCore.Qt.LeftButton) 356 | after_cancel_text = editing.tabs.currentWidget().text.toPlainText() 357 | 358 | assert after_cancel_text == before_cancel_text, "cancelling insert image did something to the text" 359 | 360 | file_dialog.return_value = ('toto', '') 361 | qtbot.mouseClick(editing.toolbar.widgetForAction(editing.imageInsertAction), 362 | QtCore.Qt.LeftButton) 363 | after_accept_text = editing.tabs.currentWidget().text.toPlainText() 364 | 365 | assert after_accept_text != after_cancel_text, "image insertion did not add something" 366 | assert after_accept_text.find('![](toto)') != -1, "image insertion did not add the right thing" 367 | 368 | # Test the tabs (should switch from one notebook to the other) 369 | # Check that there are two of them 370 | assert editing.tabs.count() == 2 371 | 372 | initial_index = editing.tabs.currentIndex() 373 | new_index = initial_index % 2 374 | new_notebook = editing.info.notebooks[new_index] 375 | editing.switchNotebook(new_notebook[:-3]) 376 | assert editing.tabs.currentIndex() == new_index 377 | # Test the refresh method by removing directly on the disk a file 378 | 379 | os.remove( 380 | os.path.join(editing.info.root, 'second'+EXTENSION)) 381 | editing.info.notebooks.pop( 382 | editing.info.notebooks.index('second'+EXTENSION)) 383 | editing.refresh() 384 | # There should be only one tab left 385 | assert editing.tabs.count() == 1 386 | 387 | # Check that zoom-in, zoom-out, reset size are implemented 388 | editor = editing.tabs.currentWidget() 389 | 390 | # Fontsize should be bigger 391 | editing.zoomIn() 392 | assert editor.text.currentFont().pointSize() > editor.defaultFontSize 393 | 394 | # Zoom out twice should get the Point size smaller 395 | editing.zoomOut() 396 | editing.zoomOut() 397 | assert editor.text.currentFont().pointSize() < editor.defaultFontSize 398 | 399 | # Reset should reset it... 400 | editing.resetSize() 401 | assert editor.text.currentFont().pointSize() == editor.defaultFontSize 402 | 403 | 404 | def test_preview(qtbot, parent): 405 | preview = Preview(parent) 406 | qtbot.addWidget(preview) 407 | 408 | # Load a notebook 409 | preview.loadNotebook(preview.info.notebooks[0]) 410 | 411 | # assert tagButtons contains six elements 412 | assert len(preview.tagButtons) == 6 413 | assert isinstance(preview.tagButtons[0][1], QtGui.QPushButton) 414 | 415 | # Click on the first tag button 416 | first_key, first_button = preview.tagButtons[0] 417 | qtbot.mouseClick(first_button, QtCore.Qt.LeftButton) 418 | 419 | assert len(preview.filters) == 1 420 | assert preview.filters[0] == first_key 421 | 422 | # Click on another, disabled button 423 | for key, button in preview.tagButtons: 424 | if key not in preview.remaining_tags: 425 | qtbot.mouseClick(button, QtCore.Qt.LeftButton) 426 | assert len(preview.filters) == 1 427 | 428 | # Add another filter 429 | for key, button in preview.tagButtons: 430 | if key in preview.remaining_tags and key != first_key: 431 | newFilter = [key, button] 432 | break 433 | 434 | qtbot.mouseClick(newFilter[1], QtCore.Qt.LeftButton) 435 | assert len(preview.filters) == 2 436 | 437 | # Unclick both 438 | qtbot.mouseClick(newFilter[1], QtCore.Qt.LeftButton) 439 | qtbot.mouseClick(first_button, QtCore.Qt.LeftButton) 440 | 441 | # Test zoom 442 | preview.zoomIn() 443 | preview.zoomOut() 444 | preview.resetSize() 445 | 446 | # Reload should work (how to test that it truly works?) 447 | preview.reload() 448 | 449 | # check searchField 450 | # isVisibleTo(preview) is needed because qtbot doesn't show the actual 451 | # root-widget (here: preview) 452 | # so we can check if the widget in question and any parent up to but 453 | # excluding the given ancestor are not hidden themselves 454 | # see documentation for Pyside.QtGui.QWidget.isVisibleTo(arg__1) 455 | # 456 | # all buttons should be visible 457 | assert len([key for key, button in preview.tagButtons if 458 | button.isVisibleTo(preview)]) == 6 459 | # filter out all but one 460 | qtbot.keyClicks(preview.searchField, 'b') 461 | assert len([key for key, button in preview.tagButtons if 462 | button.isVisibleTo(preview)]) == 2 463 | # filter out all 464 | qtbot.keyClicks(preview.searchField, 'bbbb') 465 | assert len([key for key, button in preview.tagButtons if 466 | button.isVisibleTo(preview)]) == 0 467 | # show all again 468 | preview.searchField.clear() 469 | assert len([key for key, button in preview.tagButtons if 470 | button.isVisibleTo(preview)]) == 6 471 | -------------------------------------------------------------------------------- /noteorganiser/assets/style/fonts/inconsolata/inconsolata-demo.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | Inconsolata Medium Specimen 19 | 20 | 21 | 26 | 27 | 28 | 29 |
30 | 32 | 39 | 40 |
41 | 42 | 43 |
44 | 45 |
46 |
47 |
AaBb
48 |
49 |
50 | 51 |
52 |
A​B​C​D​E​F​G​H​I​J​K​L​M​N​O​P​Q​R​S​T​U​V​W​X​Y​Z​a​b​c​d​e​f​g​h​i​j​k​l​m​n​o​p​q​r​s​t​u​v​w​x​y​z​1​2​3​4​5​6​7​8​9​0​&​.​,​?​!​@​(​)​#​$​%​*​+​-​=​:​;
53 |
54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
10abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
11abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
12abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
13abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
14abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
16abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
18abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
20abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
24abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
30abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
36abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
48abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
60abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
72abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
90abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
73 | 74 |
75 | 76 |
77 | 78 | 79 | 80 |
81 | 82 | 83 |
84 |
◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼body
body
body
body
85 |
86 | bodyInconsolata Medium 87 |
88 |
89 | bodyArial 90 |
91 |
92 | bodyVerdana 93 |
94 |
95 | bodyGeorgia 96 |
97 | 98 | 99 | 100 |
101 | 102 | 103 |
104 | 105 |
106 |

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.

217 |
218 |
219 | 220 |
221 | 222 | 223 | 224 | 225 |
226 | 227 |
228 | 229 |
230 | 231 |
232 |

Lorem Ipsum Dolor

233 |

Etiam porta sem malesuada magna mollis euismod

234 | 235 | 236 |
237 |
238 |
239 |
240 |

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 | 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.

296 |
297 | 298 |

&#32;

299 |

&#33;

!
300 |

&#34;

"
301 |

&#35;

#
302 |

&#36;

$
303 |

&#37;

%
304 |

&#38;

&
305 |

&#39;

'
306 |

&#40;

(
307 |

&#41;

)
308 |

&#42;

*
309 |

&#43;

+
310 |

&#44;

,
311 |

&#45;

-
312 |

&#46;

.
313 |

&#47;

/
314 |

&#48;

0
315 |

&#49;

1
316 |

&#50;

2
317 |

&#51;

3
318 |

&#52;

4
319 |

&#53;

5
320 |

&#54;

6
321 |

&#55;

7
322 |

&#56;

8
323 |

&#57;

9
324 |

&#58;

:
325 |

&#59;

;
326 |

&#60;

<
327 |

&#61;

=
328 |

&#62;

>
329 |

&#63;

?
330 |

&#64;

@
331 |

&#65;

A
332 |

&#66;

B
333 |

&#67;

C
334 |

&#68;

D
335 |

&#69;

E
336 |

&#70;

F
337 |

&#71;

G
338 |

&#72;

H
339 |

&#73;

I
340 |

&#74;

J
341 |

&#75;

K
342 |

&#76;

L
343 |

&#77;

M
344 |

&#78;

N
345 |

&#79;

O
346 |

&#80;

P
347 |

&#81;

Q
348 |

&#82;

R
349 |

&#83;

S
350 |

&#84;

T
351 |

&#85;

U
352 |

&#86;

V
353 |

&#87;

W
354 |

&#88;

X
355 |

&#89;

Y
356 |

&#90;

Z
357 |

&#91;

[
358 |

&#92;

\
359 |

&#93;

]
360 |

&#94;

^
361 |

&#95;

_
362 |

&#96;

`
363 |

&#97;

a
364 |

&#98;

b
365 |

&#99;

c
366 |

&#100;

d
367 |

&#101;

e
368 |

&#102;

f
369 |

&#103;

g
370 |

&#104;

h
371 |

&#105;

i
372 |

&#106;

j
373 |

&#107;

k
374 |

&#108;

l
375 |

&#109;

m
376 |

&#110;

n
377 |

&#111;

o
378 |

&#112;

p
379 |

&#113;

q
380 |

&#114;

r
381 |

&#115;

s
382 |

&#116;

t
383 |

&#117;

u
384 |

&#118;

v
385 |

&#119;

w
386 |

&#120;

x
387 |

&#121;

y
388 |

&#122;

z
389 |

&#123;

{
390 |

&#124;

|
391 |

&#125;

}
392 |

&#126;

~
393 |

&#160;

 
394 |

&#161;

¡
395 |

&#162;

¢
396 |

&#163;

£
397 |

&#164;

¤
398 |

&#165;

¥
399 |

&#166;

¦
400 |

&#167;

§
401 |

&#168;

¨
402 |

&#169;

©
403 |

&#170;

ª
404 |

&#171;

«
405 |

&#172;

¬
406 |

&#173;

­
407 |

&#174;

®
408 |

&#175;

¯
409 |

&#176;

°
410 |

&#177;

±
411 |

&#178;

²
412 |

&#179;

³
413 |

&#180;

´
414 |

&#181;

µ
415 |

&#182;

416 |

&#183;

·
417 |

&#184;

¸
418 |

&#185;

¹
419 |

&#186;

º
420 |

&#187;

»
421 |

&#188;

¼
422 |

&#189;

½
423 |

&#190;

¾
424 |

&#191;

¿
425 |

&#192;

À
426 |

&#193;

Á
427 |

&#194;

Â
428 |

&#195;

Ã
429 |

&#196;

Ä
430 |

&#197;

Å
431 |

&#198;

Æ
432 |

&#199;

Ç
433 |

&#200;

È
434 |

&#201;

É
435 |

&#202;

Ê
436 |

&#203;

Ë
437 |

&#204;

Ì
438 |

&#205;

Í
439 |

&#206;

Î
440 |

&#207;

Ï
441 |

&#208;

Ð
442 |

&#209;

Ñ
443 |

&#210;

Ò
444 |

&#211;

Ó
445 |

&#212;

Ô
446 |

&#213;

Õ
447 |

&#214;

Ö
448 |

&#215;

×
449 |

&#216;

Ø
450 |

&#217;

Ù
451 |

&#218;

Ú
452 |

&#219;

Û
453 |

&#220;

Ü
454 |

&#221;

Ý
455 |

&#222;

Þ
456 |

&#223;

ß
457 |

&#224;

à
458 |

&#225;

á
459 |

&#226;

â
460 |

&#227;

ã
461 |

&#228;

ä
462 |

&#229;

å
463 |

&#230;

æ
464 |

&#231;

ç
465 |

&#232;

è
466 |

&#233;

é
467 |

&#234;

ê
468 |

&#235;

ë
469 |

&#236;

ì
470 |

&#237;

í
471 |

&#238;

î
472 |

&#239;

ï
473 |

&#240;

ð
474 |

&#241;

ñ
475 |

&#242;

ò
476 |

&#243;

ó
477 |

&#244;

ô
478 |

&#245;

õ
479 |

&#246;

ö
480 |

&#247;

÷
481 |

&#248;

ø
482 |

&#249;

ù
483 |

&#250;

ú
484 |

&#251;

û
485 |

&#252;

ü
486 |

&#253;

ý
487 |

&#254;

þ
488 |

&#255;

ÿ
489 |

&#338;

Œ
490 |

&#339;

œ
491 |

&#376;

Ÿ
492 |

&#710;

ˆ
493 |

&#732;

˜
494 |

&#8192;

 
495 |

&#8193;

496 |

&#8194;

497 |

&#8195;

498 |

&#8196;

499 |

&#8197;

500 |

&#8198;

501 |

&#8199;

502 |

&#8200;

503 |

&#8201;

504 |

&#8202;

505 |

&#8208;

506 |

&#8209;

507 |

&#8210;

508 |

&#8211;

509 |

&#8212;

510 |

&#8216;

511 |

&#8217;

512 |

&#8218;

513 |

&#8220;

514 |

&#8221;

515 |

&#8222;

516 |

&#8226;

517 |

&#8230;

518 |

&#8239;

519 |

&#8249;

520 |

&#8250;

521 |

&#8287;

522 |

&#8364;

523 |

&#8482;

524 |

&#9724;

525 |
526 |
527 | 528 | 529 |
530 |
531 | 532 | 533 |
534 | 535 |
536 | 537 |
538 |
539 |
540 |

Installing Webfonts

541 | 542 |

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:

549 | 550 | 551 | 552 | @font-face{ 553 | font-family: 'MyWebFont'; 554 | src: url('WebFont.eot'); 555 | src: url('WebFont.eot?#iefix') format('embedded-opentype'), 556 | url('WebFont.woff') format('woff'), 557 | url('WebFont.ttf') format('truetype'), 558 | url('WebFont.svg#webfont') format('svg'); 559 | } 560 | 561 | 562 |

We've already gone ahead and generated the code for you. All you have to do is link to the stylesheet in your HTML, like this:

563 | <link rel="stylesheet" href="stylesheet.css" type="text/css" charset="utf-8" /> 564 | 565 |

3. Modify your own stylesheet

566 |

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:

567 | p { font-family: 'WebFont', Arial, sans-serif; } 568 | 569 |

4. Test

570 |

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 | 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 | --------------------------------------------------------------------------------