├── .gitignore ├── COPYING ├── MANIFEST.in ├── data ├── qnotero.desktop └── qnotero.ico ├── debian ├── changelog ├── compat ├── control ├── copyright └── rules ├── libqnotero ├── __init__.py ├── _themes │ ├── __init__.py │ ├── chameleon.py │ ├── default.py │ ├── defaultframed.py │ ├── elementary.py │ └── tango.py ├── config.py ├── listener.py ├── preferences.py ├── qnotero.py ├── qnoteroException.py ├── qnoteroItem.py ├── qnoteroItemDelegate.py ├── qnoteroQuery.py ├── qnoteroResults.py ├── qt │ ├── QtCore.py │ ├── QtGui.py │ ├── __init__.py │ └── uic.py ├── sysTray.py ├── ui │ ├── preferences.ui │ └── qnotero.ui └── uiloader.py ├── libzotero ├── __init__.py ├── _noteProvider │ ├── __init__.py │ └── gnoteProvider.py ├── libzotero.py └── zotero_item.py ├── qnotero ├── qnotero.nsi ├── readme-src.md ├── readme.md ├── readme.py ├── resources ├── default │ ├── close.png │ ├── nopdf.png │ ├── pdf.png │ ├── preferences.png │ ├── qnotero.png │ ├── search.png │ └── stylesheet.qss ├── elementary │ ├── close.svg │ ├── nopdf.svg │ ├── pdf.svg │ ├── preferences.svg │ ├── qnotero.svg │ ├── search.svg │ └── stylesheet.qss └── tango │ ├── close.png │ ├── nopdf.png │ ├── pdf.png │ ├── preferences.png │ ├── qnotero.png │ ├── search.png │ └── stylesheet.qss ├── setup-windows.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | ~* 3 | dist 4 | build 5 | .* 6 | releases 7 | 8 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | NOTE! The GPL below is copyrighted by the Free Software Foundation, but 2 | the instance of code that it refers to (Qnotero) are copyrighted 3 | by Sebastiaan Mathot . 4 | 5 | --------------------------------------------------------------------------- 6 | 7 | GNU GENERAL PUBLIC LICENSE 8 | Version 2, June 1991 9 | 10 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 11 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 12 | Everyone is permitted to copy and distribute verbatim copies 13 | of this license document, but changing it is not allowed. 14 | 15 | Preamble 16 | 17 | The licenses for most software are designed to take away your 18 | freedom to share and change it. By contrast, the GNU General Public 19 | License is intended to guarantee your freedom to share and change free 20 | software--to make sure the software is free for all its users. This 21 | General Public License applies to most of the Free Software 22 | Foundation's software and to any other program whose authors commit to 23 | using it. (Some other Free Software Foundation software is covered by 24 | the GNU Library General Public License instead.) You can apply it to 25 | your programs, too. 26 | 27 | When we speak of free software, we are referring to freedom, not 28 | price. Our General Public Licenses are designed to make sure that you 29 | have the freedom to distribute copies of free software (and charge for 30 | this service if you wish), that you receive source code or can get it 31 | if you want it, that you can change the software or use pieces of it 32 | in new free programs; and that you know you can do these things. 33 | 34 | To protect your rights, we need to make restrictions that forbid 35 | anyone to deny you these rights or to ask you to surrender the rights. 36 | These restrictions translate to certain responsibilities for you if you 37 | distribute copies of the software, or if you modify it. 38 | 39 | For example, if you distribute copies of such a program, whether 40 | gratis or for a fee, you must give the recipients all the rights that 41 | you have. You must make sure that they, too, receive or can get the 42 | source code. And you must show them these terms so they know their 43 | rights. 44 | 45 | We protect your rights with two steps: (1) copyright the software, and 46 | (2) offer you this license which gives you legal permission to copy, 47 | distribute and/or modify the software. 48 | 49 | Also, for each author's protection and ours, we want to make certain 50 | that everyone understands that there is no warranty for this free 51 | software. If the software is modified by someone else and passed on, we 52 | want its recipients to know that what they have is not the original, so 53 | that any problems introduced by others will not reflect on the original 54 | authors' reputations. 55 | 56 | Finally, any free program is threatened constantly by software 57 | patents. We wish to avoid the danger that redistributors of a free 58 | program will individually obtain patent licenses, in effect making the 59 | program proprietary. To prevent this, we have made it clear that any 60 | patent must be licensed for everyone's free use or not licensed at all. 61 | 62 | The precise terms and conditions for copying, distribution and 63 | modification follow. 64 | 65 | GNU GENERAL PUBLIC LICENSE 66 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 67 | 68 | 0. This License applies to any program or other work which contains 69 | a notice placed by the copyright holder saying it may be distributed 70 | under the terms of this General Public License. The "Program", below, 71 | refers to any such program or work, and a "work based on the Program" 72 | means either the Program or any derivative work under copyright law: 73 | that is to say, a work containing the Program or a portion of it, 74 | either verbatim or with modifications and/or translated into another 75 | language. (Hereinafter, translation is included without limitation in 76 | the term "modification".) Each licensee is addressed as "you". 77 | 78 | Activities other than copying, distribution and modification are not 79 | covered by this License; they are outside its scope. The act of 80 | running the Program is not restricted, and the output from the Program 81 | is covered only if its contents constitute a work based on the 82 | Program (independent of having been made by running the Program). 83 | Whether that is true depends on what the Program does. 84 | 85 | 1. You may copy and distribute verbatim copies of the Program's 86 | source code as you receive it, in any medium, provided that you 87 | conspicuously and appropriately publish on each copy an appropriate 88 | copyright notice and disclaimer of warranty; keep intact all the 89 | notices that refer to this License and to the absence of any warranty; 90 | and give any other recipients of the Program a copy of this License 91 | along with the Program. 92 | 93 | You may charge a fee for the physical act of transferring a copy, and 94 | you may at your option offer warranty protection in exchange for a fee. 95 | 96 | 2. You may modify your copy or copies of the Program or any portion 97 | of it, thus forming a work based on the Program, and copy and 98 | distribute such modifications or work under the terms of Section 1 99 | above, provided that you also meet all of these conditions: 100 | 101 | a) You must cause the modified files to carry prominent notices 102 | stating that you changed the files and the date of any change. 103 | 104 | b) You must cause any work that you distribute or publish, that in 105 | whole or in part contains or is derived from the Program or any 106 | part thereof, to be licensed as a whole at no charge to all third 107 | parties under the terms of this License. 108 | 109 | c) If the modified program normally reads commands interactively 110 | when run, you must cause it, when started running for such 111 | interactive use in the most ordinary way, to print or display an 112 | announcement including an appropriate copyright notice and a 113 | notice that there is no warranty (or else, saying that you provide 114 | a warranty) and that users may redistribute the program under 115 | these conditions, and telling the user how to view a copy of this 116 | License. (Exception: if the Program itself is interactive but 117 | does not normally print such an announcement, your work based on 118 | the Program is not required to print an announcement.) 119 | 120 | These requirements apply to the modified work as a whole. If 121 | identifiable sections of that work are not derived from the Program, 122 | and can be reasonably considered independent and separate works in 123 | themselves, then this License, and its terms, do not apply to those 124 | sections when you distribute them as separate works. But when you 125 | distribute the same sections as part of a whole which is a work based 126 | on the Program, the distribution of the whole must be on the terms of 127 | this License, whose permissions for other licensees extend to the 128 | entire whole, and thus to each and every part regardless of who wrote it. 129 | 130 | Thus, it is not the intent of this section to claim rights or contest 131 | your rights to work written entirely by you; rather, the intent is to 132 | exercise the right to control the distribution of derivative or 133 | collective works based on the Program. 134 | 135 | In addition, mere aggregation of another work not based on the Program 136 | with the Program (or with a work based on the Program) on a volume of 137 | a storage or distribution medium does not bring the other work under 138 | the scope of this License. 139 | 140 | 3. You may copy and distribute the Program (or a work based on it, 141 | under Section 2) in object code or executable form under the terms of 142 | Sections 1 and 2 above provided that you also do one of the following: 143 | 144 | a) Accompany it with the complete corresponding machine-readable 145 | source code, which must be distributed under the terms of Sections 146 | 1 and 2 above on a medium customarily used for software interchange; or, 147 | 148 | b) Accompany it with a written offer, valid for at least three 149 | years, to give any third party, for a charge no more than your 150 | cost of physically performing source distribution, a complete 151 | machine-readable copy of the corresponding source code, to be 152 | distributed under the terms of Sections 1 and 2 above on a medium 153 | customarily used for software interchange; or, 154 | 155 | c) Accompany it with the information you received as to the offer 156 | to distribute corresponding source code. (This alternative is 157 | allowed only for noncommercial distribution and only if you 158 | received the program in object code or executable form with such 159 | an offer, in accord with Subsection b above.) 160 | 161 | The source code for a work means the preferred form of the work for 162 | making modifications to it. For an executable work, complete source 163 | code means all the source code for all modules it contains, plus any 164 | associated interface definition files, plus the scripts used to 165 | control compilation and installation of the executable. However, as a 166 | special exception, the source code distributed need not include 167 | anything that is normally distributed (in either source or binary 168 | form) with the major components (compiler, kernel, and so on) of the 169 | operating system on which the executable runs, unless that component 170 | itself accompanies the executable. 171 | 172 | If distribution of executable or object code is made by offering 173 | access to copy from a designated place, then offering equivalent 174 | access to copy the source code from the same place counts as 175 | distribution of the source code, even though third parties are not 176 | compelled to copy the source along with the object code. 177 | 178 | 4. You may not copy, modify, sublicense, or distribute the Program 179 | except as expressly provided under this License. Any attempt 180 | otherwise to copy, modify, sublicense or distribute the Program is 181 | void, and will automatically terminate your rights under this License. 182 | However, parties who have received copies, or rights, from you under 183 | this License will not have their licenses terminated so long as such 184 | parties remain in full compliance. 185 | 186 | 5. You are not required to accept this License, since you have not 187 | signed it. However, nothing else grants you permission to modify or 188 | distribute the Program or its derivative works. These actions are 189 | prohibited by law if you do not accept this License. Therefore, by 190 | modifying or distributing the Program (or any work based on the 191 | Program), you indicate your acceptance of this License to do so, and 192 | all its terms and conditions for copying, distributing or modifying 193 | the Program or works based on it. 194 | 195 | 6. Each time you redistribute the Program (or any work based on the 196 | Program), the recipient automatically receives a license from the 197 | original licensor to copy, distribute or modify the Program subject to 198 | these terms and conditions. You may not impose any further 199 | restrictions on the recipients' exercise of the rights granted herein. 200 | You are not responsible for enforcing compliance by third parties to 201 | this License. 202 | 203 | 7. If, as a consequence of a court judgment or allegation of patent 204 | infringement or for any other reason (not limited to patent issues), 205 | conditions are imposed on you (whether by court order, agreement or 206 | otherwise) that contradict the conditions of this License, they do not 207 | excuse you from the conditions of this License. If you cannot 208 | distribute so as to satisfy simultaneously your obligations under this 209 | License and any other pertinent obligations, then as a consequence you 210 | may not distribute the Program at all. For example, if a patent 211 | license would not permit royalty-free redistribution of the Program by 212 | all those who receive copies directly or indirectly through you, then 213 | the only way you could satisfy both it and this License would be to 214 | refrain entirely from distribution of the Program. 215 | 216 | If any portion of this section is held invalid or unenforceable under 217 | any particular circumstance, the balance of the section is intended to 218 | apply and the section as a whole is intended to apply in other 219 | circumstances. 220 | 221 | It is not the purpose of this section to induce you to infringe any 222 | patents or other property right claims or to contest validity of any 223 | such claims; this section has the sole purpose of protecting the 224 | integrity of the free software distribution system, which is 225 | implemented by public license practices. Many people have made 226 | generous contributions to the wide range of software distributed 227 | through that system in reliance on consistent application of that 228 | system; it is up to the author/donor to decide if he or she is willing 229 | to distribute software through any other system and a licensee cannot 230 | impose that choice. 231 | 232 | This section is intended to make thoroughly clear what is believed to 233 | be a consequence of the rest of this License. 234 | 235 | 8. If the distribution and/or use of the Program is restricted in 236 | certain countries either by patents or by copyrighted interfaces, the 237 | original copyright holder who places the Program under this License 238 | may add an explicit geographical distribution limitation excluding 239 | those countries, so that distribution is permitted only in or among 240 | countries not thus excluded. In such case, this License incorporates 241 | the limitation as if written in the body of this License. 242 | 243 | 9. The Free Software Foundation may publish revised and/or new versions 244 | of the General Public License from time to time. Such new versions will 245 | be similar in spirit to the present version, but may differ in detail to 246 | address new problems or concerns. 247 | 248 | Each version is given a distinguishing version number. If the Program 249 | specifies a version number of this License which applies to it and "any 250 | later version", you have the option of following the terms and conditions 251 | either of that version or of any later version published by the Free 252 | Software Foundation. If the Program does not specify a version number of 253 | this License, you may choose any version ever published by the Free Software 254 | Foundation. 255 | 256 | 10. If you wish to incorporate parts of the Program into other free 257 | programs whose distribution conditions are different, write to the author 258 | to ask for permission. For software which is copyrighted by the Free 259 | Software Foundation, write to the Free Software Foundation; we sometimes 260 | make exceptions for this. Our decision will be guided by the two goals 261 | of preserving the free status of all derivatives of our free software and 262 | of promoting the sharing and reuse of software generally. 263 | 264 | NO WARRANTY 265 | 266 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 267 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 268 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 269 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 270 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 271 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 272 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 273 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 274 | REPAIR OR CORRECTION. 275 | 276 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 277 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 278 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 279 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 280 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 281 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 282 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 283 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 284 | POSSIBILITY OF SUCH DAMAGES. 285 | 286 | END OF TERMS AND CONDITIONS 287 | 288 | 289 | How to Apply These Terms to Your New Programs 290 | 291 | If you develop a new program, and you want it to be of the greatest 292 | possible use to the public, the best way to achieve this is to make it 293 | free software which everyone can redistribute and change under these terms. 294 | 295 | To do so, attach the following notices to the program. It is safest 296 | to attach them to the start of each source file to most effectively 297 | convey the exclusion of warranty; and each file should have at least 298 | the "copyright" line and a pointer to where the full notice is found. 299 | 300 | 301 | Copyright (C) 19yy 302 | 303 | This program is free software; you can redistribute it and/or modify 304 | it under the terms of the GNU General Public License as published by 305 | the Free Software Foundation; either version 2 of the License, or 306 | (at your option) any later version. 307 | 308 | This program is distributed in the hope that it will be useful, 309 | but WITHOUT ANY WARRANTY; without even the implied warranty of 310 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 311 | GNU General Public License for more details. 312 | 313 | You should have received a copy of the GNU General Public License 314 | along with this program; if not, write to the Free Software 315 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 316 | 317 | 318 | Also add information on how to contact you by electronic and paper mail. 319 | 320 | If the program is interactive, make it output a short notice like this 321 | when it starts in an interactive mode: 322 | 323 | Gnomovision version 69, Copyright (C) 19yy name of author 324 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 325 | This is free software, and you are welcome to redistribute it 326 | under certain conditions; type `show c' for details. 327 | 328 | The hypothetical commands `show w' and `show c' should show the appropriate 329 | parts of the General Public License. Of course, the commands you use may 330 | be called something other than `show w' and `show c'; they could even be 331 | mouse-clicks or menu items--whatever suits your program. 332 | 333 | You should also get your employer (if you work as a programmer) or your 334 | school, if any, to sign a "copyright disclaimer" for the program, if 335 | necessary. Here is a sample; alter the names: 336 | 337 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 338 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 339 | 340 | , 1 April 1989 341 | Ty Coon, President of Vice 342 | 343 | This General Public License does not permit incorporating your program into 344 | proprietary programs. If your program is a subroutine library, you may 345 | consider it more useful to permit linking proprietary applications with the 346 | library. If this is what you want to do, use the GNU Library General 347 | Public License instead of this License. 348 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include data/qnotero.desktop 2 | recursive-include resources 3 | recursive-exclude *.pyc 4 | recursive-include ~* -------------------------------------------------------------------------------- /data/qnotero.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Qnotero 3 | Comment=Quick access to your Zotero references 4 | TryExec=qnotero 5 | Exec=qnotero 6 | Icon=accessories-dictionary 7 | Type=Application 8 | X-GNOME-DocPath= 9 | X-GNOME-Bugzilla-Bugzilla= 10 | X-GNOME-Bugzilla-Product= 11 | X-GNOME-Bugzilla-Component= 12 | X-GNOME-Bugzilla-Version= 13 | Categories=GNOME;GTK;Utility; 14 | StartupNotify=false 15 | X-Ubuntu-Gettext-Domain=qnotero 16 | Name[en_US]=Qnotero 17 | 18 | -------------------------------------------------------------------------------- /data/qnotero.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/data/qnotero.ico -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | qnotero (2.0.0-ubuntu2) utopic; urgency=medium 2 | 3 | * Ported to PyQt5 4 | * Properly capitilize names (#18) 5 | * Don't crash when title is number (#19) 6 | * Add Ctrl+F shortcut to focus search box when activated (#20) 7 | 8 | -- Sebastiaan Mathot Wed, 07 Jan 2015 16:13:12 +0100 9 | 10 | qnotero (1.0.0-ubuntu7) trusty; urgency=medium 11 | 12 | * Ported to Python 3 13 | * Add tag support 14 | * Do not treat editors as authors 15 | * Various bug-fixes 16 | 17 | -- Sebastiaan Mathot Tue, 19 Aug 2014 16:43:29 +0200 18 | 19 | qnotero (0.48-ubuntu1) oneiric; urgency=low 20 | 21 | [ Sebastiaan Mathot ] 22 | * Various bug-fixes 23 | 24 | -- Sebastiaan Mathot Thu, 01 Mar 2012 16:40:00 +0100 25 | 26 | qnotero (0.47-ubuntu1) oneiric; urgency=low 27 | 28 | [ Sebastiaan Mathot ] 29 | * Initial release. 30 | 31 | -- Sebastiaan Mathot Mon, 07 Nov 2011 21:54:47 +0100 32 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: qnotero 2 | Section: science 3 | Priority: extra 4 | Maintainer: Sebastiaan Mathot 5 | Uploaders: Sebastiaan Mathot 6 | Build-Depends: debhelper (>= 7.0.50~), python3-all, python3-pyqt5 7 | X-Python3-Version: >= 3.3 8 | Standards-Version: 3.9.1 9 | Homepage: http://www.cogsci.nl/ 10 | 11 | Package: qnotero 12 | Architecture: all 13 | X-Python3-Version: ${python3:Versions} 14 | Depends: ${misc:Depends}, ${python3:Depends}, python3-pyqt5, 15 | python3-levenshtein 16 | Recommends: 17 | Suggests: xul-ext-zotero, zotero-standalone 18 | Description: Qnotero is a sidekick to Zotero, the open-source reference manager. 19 | Zotero is an excellent tool for managing your reference, but it lacks a simple 20 | and direct way to search your references and open attached PDFs at the click 21 | of a button. Qnotero lives in the system tray of your desktop and provides 22 | quick access to your Zotero database, even if Zotero is not running. 23 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135 2 | Name: qnotero 3 | Maintainer: Sebastiaan Mathot 4 | Source: http://www.cogsci.nl/ 5 | 6 | Files: * 7 | Copyright: 2010-2011, Sebastiaan Mathot 8 | License: GPL-3 9 | On Debian systems, the full text of the GNU General Public License version 3 10 | can be found in the file `/usr/share/common-licenses/GPL-3'. 11 | 12 | Files: resources/default/*.png 13 | Copyright: 2010-2011 Matthieu James 14 | Licenses: GPL-3 15 | 16 | Files: resources/elementary/*.png 17 | Copyright: 2010-2011 DanRabbit 18 | Licenses: GPL-3 19 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | export DH_VERBOSE=1 2 | export PYBUILD_NAME=qnotero 3 | 4 | %: 5 | dh $@ --with python3 --buildsystem=pybuild 6 | -------------------------------------------------------------------------------- /libqnotero/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/libqnotero/__init__.py -------------------------------------------------------------------------------- /libqnotero/_themes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/libqnotero/_themes/__init__.py -------------------------------------------------------------------------------- /libqnotero/_themes/chameleon.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | from libqnotero._themes.default import Default 19 | from libqnotero.qt.QtGui import QIcon, QPixmap, QLabel 20 | from libqnotero.qt.QtCore import Qt 21 | 22 | class Chameleon(Default): 23 | 24 | """A theme that blends in with the desktop""" 25 | 26 | mapping = {"qnotero" : "accessories-dictionary", \ 27 | "close" : "window-close", \ 28 | "pdf" : "application-pdf", \ 29 | "nopdf" : "text-plain", \ 30 | "preferences" : "preferences-other", \ 31 | "search" : "system-search", \ 32 | } 33 | 34 | def __init__(self, qnotero): 35 | 36 | """ 37 | Constructor 38 | 39 | qnotero -- a Qnotero instance 40 | """ 41 | 42 | Default.__init__(self, qnotero) 43 | self.qnotero.ui.lineEditQuery.setFrame(True) 44 | 45 | def icon(self, iconName): 46 | 47 | """ 48 | Retrieves an icon from the theme 49 | 50 | Arguments: 51 | iconName -- the name of the icon 52 | 53 | Returns: 54 | A QIcon 55 | """ 56 | 57 | if iconName in self.mapping: 58 | icon = self.mapping[iconName] 59 | else: 60 | icon = "edit-undo" 61 | if not QIcon.hasThemeIcon(icon): 62 | print("libqnotero._themes.icon(): failed to find '%s'" % icon) 63 | return QIcon.fromTheme("icon", Default.icon(self, iconName)) 64 | 65 | def pixmap(self, pixmapName): 66 | 67 | """ 68 | Retrieves an icon (as QPixmap) from the theme 69 | 70 | Arguments: 71 | pixmapName -- the name of the icon 72 | 73 | Returns: 74 | A QPixmap 75 | """ 76 | 77 | icon = self.icon(pixmapName) 78 | return icon.pixmap(32,32) 79 | 80 | def setStyleSheet(self): 81 | 82 | """Applies a stylesheet to Qnotero""" 83 | 84 | pass 85 | 86 | def setScrollBars(self): 87 | 88 | """Set the scrollbar properties""" 89 | 90 | self.qnotero.ui.listWidgetResults.setHorizontalScrollBarPolicy( \ 91 | Qt.ScrollBarAlwaysOff) 92 | 93 | -------------------------------------------------------------------------------- /libqnotero/_themes/default.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | import sys 19 | import os 20 | import os.path 21 | from libqnotero.qnoteroException import QnoteroException 22 | from libqnotero.qt.QtGui import QIcon, QPixmap 23 | from libqnotero.qt.QtGui import QLabel 24 | from libqnotero.qt.QtCore import Qt 25 | 26 | class Default: 27 | 28 | """The default Qnotero theme""" 29 | 30 | def __init__(self, qnotero): 31 | 32 | """ 33 | Constructor 34 | 35 | qnotero -- a Qnotero instance 36 | """ 37 | 38 | self.qnotero = qnotero 39 | self.setThemeFolder() 40 | self.setStyleSheet() 41 | self.setWindowProperties() 42 | self.setScrollBars() 43 | 44 | def icon(self, iconName): 45 | 46 | """ 47 | Retrieves an icon from the theme 48 | 49 | Arguments: 50 | iconName -- the name of the icon 51 | 52 | Returns: 53 | A QIcon 54 | """ 55 | 56 | return QIcon(os.path.join(self._themeFolder, iconName) \ 57 | + self._iconExt) 58 | 59 | def iconExt(self): 60 | 61 | """ 62 | Determines the file format of the icons 63 | 64 | Returns: 65 | An extension (.png, .svg, etc.) 66 | """ 67 | 68 | return ".png" 69 | 70 | def iconWidget(self, iconName): 71 | 72 | """ 73 | Return a QLabel with an icon 74 | 75 | Arguments: 76 | iconName -- the name of the icon 77 | 78 | Returns: 79 | A QLabel 80 | """ 81 | 82 | l = QLabel() 83 | l.setPixmap(self.pixmap(iconName)) 84 | return l 85 | 86 | def lineHeight(self): 87 | 88 | """ 89 | Determines the line height of the results 90 | 91 | Returns: 92 | A float (e.g., 1.1) for the line height 93 | """ 94 | 95 | return 1.1 96 | 97 | def pixmap(self, pixmapName): 98 | 99 | """ 100 | Retrieves an icon (as QPixmap) from the theme 101 | 102 | Arguments: 103 | pixmapName -- the name of the icon 104 | 105 | Returns: 106 | A QPixmap 107 | """ 108 | 109 | return QPixmap(os.path.join(self._themeFolder, pixmapName) \ 110 | + self._iconExt) 111 | 112 | def roundness(self): 113 | 114 | """ 115 | Determines the roundness of various widgets 116 | 117 | Returns: 118 | A roundness as a radius in pixels 119 | """ 120 | 121 | return 10 122 | 123 | def setScrollBars(self): 124 | 125 | """Set the scrollbar properties""" 126 | 127 | self.qnotero.ui.listWidgetResults.setHorizontalScrollBarPolicy( \ 128 | Qt.ScrollBarAlwaysOff) 129 | self.qnotero.ui.listWidgetResults.setVerticalScrollBarPolicy( \ 130 | Qt.ScrollBarAlwaysOff) 131 | 132 | def setStyleSheet(self): 133 | 134 | """Applies a stylesheet to Qnotero""" 135 | 136 | self.qnotero.setStyleSheet(open(os.path.join( \ 137 | self._themeFolder, "stylesheet.qss")).read()) 138 | 139 | def setThemeFolder(self): 140 | 141 | """Initialize the theme folder""" 142 | 143 | self._themeFolder = os.path.join(os.path.dirname(sys.argv[0]), \ 144 | "resources", self.themeFolder()) 145 | self._iconExt = self.iconExt() 146 | if not os.path.exists(self._themeFolder): 147 | self._themeFolder = os.path.join("/usr/share/qnotero/resources/", \ 148 | self.themeFolder()) 149 | if not os.path.exists(self._themeFolder): 150 | raise QnoteroException("Failed to find resource folder!") 151 | print("libqnotero._themes.default.__init__(): using '%s'" \ 152 | % self._themeFolder) 153 | 154 | def setWindowProperties(self): 155 | 156 | """Set the window properties (frameless, etc.)""" 157 | 158 | self.qnotero.setWindowFlags(Qt.Popup) 159 | 160 | def themeFolder(self): 161 | 162 | """ 163 | Determines the name of the folder containing the theme resources 164 | 165 | Returns: 166 | The name of the theme folder 167 | """ 168 | 169 | return "default" 170 | -------------------------------------------------------------------------------- /libqnotero/_themes/defaultframed.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | from libqnotero._themes.default import Default 19 | 20 | class Defaultframed(Default): 21 | 22 | """The Default theme with a wondow frame""" 23 | 24 | def __init__(self, qnotero): 25 | 26 | Default.__init__(self, qnotero) 27 | 28 | def setWindowProperties(self): 29 | 30 | pass 31 | -------------------------------------------------------------------------------- /libqnotero/_themes/elementary.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | from libqnotero._themes.default import Default 19 | 20 | class Elementary(Default): 21 | 22 | """The Elementary Qnotero theme""" 23 | 24 | def __init__(self, qnotero): 25 | 26 | Default.__init__(self, qnotero) 27 | 28 | def iconExt(self): 29 | 30 | return ".svg" 31 | 32 | def roundness(self): 33 | 34 | return 2 35 | 36 | def themeFolder(self): 37 | 38 | return "elementary" 39 | -------------------------------------------------------------------------------- /libqnotero/_themes/tango.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | from libqnotero.qt.QtCore import Qt 19 | from libqnotero._themes.default import Default 20 | 21 | class Tango(Default): 22 | 23 | """The Tango Qnotero theme""" 24 | 25 | def __init__(self, qnotero): 26 | 27 | Default.__init__(self, qnotero) 28 | 29 | def themeFolder(self): 30 | 31 | return "tango" 32 | 33 | -------------------------------------------------------------------------------- /libqnotero/config.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of qnotero. 5 | 6 | qnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | qnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with qnotero. If not, see . 18 | """ 19 | 20 | import sys 21 | 22 | config = { 23 | u"autoFire" : 500, 24 | u"autoUpdateCheck" : True, 25 | u"cfgVer" : 0, 26 | u"firstRun" : True, 27 | u"listenerPort" : 43250, 28 | u"minQueryLength" : 3, 29 | u"noteProvider" : u"gnote", 30 | u"pdfReader" : u"xdg-open", 31 | u"theme" : u"Default", 32 | u"updateUrl" : \ 33 | u"http://files.cogsci.nl/software/qnotero/MOST_RECENT_VERSION.TXT", 34 | u"pos" : u"Top right", 35 | u"zoteroPath" : u"", 36 | u"mdNoteproviderPath" : u"", 37 | } 38 | 39 | def getConfig(setting): 40 | 41 | """ 42 | Retrieve a setting 43 | 44 | Returns: 45 | A setting or False if the setting does not exist 46 | """ 47 | 48 | return config[setting] 49 | 50 | def setConfig(setting, value): 51 | 52 | """ 53 | Set a setting 54 | 55 | Arguments: 56 | setting -- the setting name 57 | value -- the setting value 58 | """ 59 | 60 | config[setting] = value 61 | config[u"cfgVer"] += 1 62 | 63 | def restoreConfig(settings): 64 | 65 | """ 66 | Restore settings from a QSetting 67 | 68 | Arguments: 69 | settings -- a QSetting 70 | """ 71 | 72 | for setting, default in config.items(): 73 | if isinstance(default, bool): 74 | # Booleans are saved as lowercase true/ false strings. 75 | value = settings.value(setting, default) == 'true' 76 | elif isinstance(default, str): 77 | value = str(settings.value(setting, default)) 78 | elif isinstance(default, int): 79 | value = int(settings.value(setting, default)) 80 | elif isinstance(default, float): 81 | value = float(settings.value(setting, default)) 82 | else: 83 | raise Exception(u'Unknown default type') 84 | setConfig(setting, value) 85 | 86 | def saveConfig(settings): 87 | 88 | """ 89 | Save settings to a QSetting 90 | 91 | Arguments: 92 | setting -- a QSetting 93 | """ 94 | 95 | for setting, value in config.items(): 96 | settings.setValue(setting, value) 97 | -------------------------------------------------------------------------------- /libqnotero/listener.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | import socket 19 | from libqnotero.config import getConfig 20 | from threading import Thread 21 | 22 | class Listener(Thread): 23 | 24 | """Listens for commands""" 25 | 26 | def __init__(self, qnotero=None): 27 | 28 | """ 29 | Constructor 30 | 31 | Arguments: 32 | qnotero -- a Qnotero instance 33 | """ 34 | 35 | self.port = getConfig("listenerPort") 36 | self.qnotero = qnotero 37 | self.alive = True 38 | Thread.__init__(self) 39 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 40 | self.sock.bind(("", self.port)) 41 | self.sock.settimeout(1.) 42 | 43 | def run(self): 44 | 45 | """Listenes for activation signals and pops up the Qnotero window""" 46 | 47 | while self.alive: 48 | try: 49 | s, comm_addr = self.sock.recvfrom(128) 50 | except: 51 | s = None 52 | if s != None: 53 | print("listener.run(): received '%s'" % s) 54 | if b"activate" == s[:8]: 55 | print("listener.run(): activating") 56 | self.qnotero.sysTray.listenerActivated.emit() 57 | 58 | -------------------------------------------------------------------------------- /libqnotero/preferences.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of qnotero. 5 | 6 | qnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | qnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with qnotero. If not, see . 18 | """ 19 | 20 | import sys 21 | import os 22 | import os.path 23 | import pkgutil 24 | from libqnotero.qt.QtGui import QDialog, QFileDialog, QMessageBox, QApplication 25 | from libqnotero.qt import uic 26 | from libqnotero.config import getConfig, setConfig 27 | from libqnotero.uiloader import UiLoader 28 | from libzotero.libzotero import valid_location 29 | 30 | class Preferences(QDialog, UiLoader): 31 | 32 | """Qnotero preferences dialog""" 33 | 34 | def __init__(self, qnotero, firstRun=False): 35 | 36 | """ 37 | Constructor 38 | 39 | Arguments: 40 | qnotero -- a Qnotero instance 41 | 42 | Keyword arguments: 43 | firstRun -- indicates if the first run message should be shown 44 | (default=False) 45 | """ 46 | 47 | QDialog.__init__(self) 48 | self.qnotero = qnotero 49 | self.loadUi('preferences') 50 | self.ui.labelLocatePath.hide() 51 | if not firstRun: 52 | self.ui.labelFirstRun.hide() 53 | self.ui.labelTitleMsg.setText( 54 | self.ui.labelTitleMsg.text().replace(u"[version]", 55 | self.qnotero.version)) 56 | self.ui.pushButtonZoteroPathAutoDetect.clicked.connect( 57 | self.zoteroPathAutoDetect) 58 | self.ui.pushButtonZoteroPathBrowse.clicked.connect( 59 | self.zoteroPathBrowse) 60 | self.ui.checkBoxAutoUpdateCheck.setChecked(getConfig(u"autoUpdateCheck")) 61 | self.ui.lineEditZoteroPath.setText(getConfig(u"zoteroPath")) 62 | i = 0 63 | import libqnotero._themes 64 | themePath = os.path.dirname(libqnotero._themes.__file__) 65 | for _, theme, _ in pkgutil.iter_modules([themePath]): 66 | self.ui.comboBoxTheme.addItem(theme) 67 | if theme == getConfig(u"theme").lower(): 68 | self.ui.comboBoxTheme.setCurrentIndex(i) 69 | i += 1 70 | self.setStyleSheet(self.qnotero.styleSheet()) 71 | self.adjustSize() 72 | 73 | def accept(self): 74 | 75 | """Accept the changes""" 76 | 77 | if self.ui.labelLocatePath.isVisible(): 78 | return 79 | print('saving!') 80 | setConfig(u"firstRun", False) 81 | setConfig(u"pos", self.ui.comboBoxPos.currentText()) 82 | setConfig(u"autoUpdateCheck", 83 | self.ui.checkBoxAutoUpdateCheck.isChecked()) 84 | setConfig(u"zoteroPath", self.ui.lineEditZoteroPath.text()) 85 | setConfig(u"theme", self.ui.comboBoxTheme.currentText().capitalize()) 86 | self.qnotero.saveState() 87 | self.qnotero.reInit() 88 | QDialog.accept(self) 89 | 90 | def locate(self, path, target): 91 | 92 | """ 93 | Tries to find the location of a target file 94 | 95 | Arguments: 96 | path -- the path to search 97 | target -- the target file 98 | 99 | Returns: 100 | The full path to the target file or None if it wasn't found 101 | """ 102 | 103 | self.ui.labelLocatePath.setText(u"Scanning: ...%s" % path[-32:]) 104 | QApplication.processEvents() 105 | # Don't scan filesystems that may contain recursions 106 | if u".gvfs" in path or u".wine" in path: 107 | return None 108 | for (dirpath, dirnames, filenames) in os.walk(path): 109 | for filename in filenames: 110 | if filename == target: 111 | return dirpath 112 | for dirname in dirnames: 113 | location = self.locate(os.path.join(dirpath, dirname), target) 114 | if location != None: 115 | return location 116 | return None 117 | 118 | def reject(self): 119 | 120 | """Reject changes""" 121 | 122 | if not self.ui.labelLocatePath.isVisible(): 123 | QDialog.reject(self) 124 | 125 | def setZoteroPath(self, path): 126 | 127 | """ 128 | Validate and set the Zotero path 129 | 130 | Arguments: 131 | path -- the Zotero path 132 | """ 133 | 134 | if valid_location(path): 135 | self.ui.lineEditZoteroPath.setText(path) 136 | else: 137 | QMessageBox.information(self, u"Invalid Zotero path", 138 | u"The folder you selected does not contain 'zotero.sqlite'") 139 | 140 | def zoteroPathAutoDetect(self): 141 | 142 | """Auto-detect the Zotero folder""" 143 | 144 | self.ui.labelLocatePath.show() 145 | if os.name == u"nt": 146 | home= os.environ[u"USERPROFILE"] 147 | elif os.name == u"posix": 148 | home = os.environ[u"HOME"] 149 | zoteroPath = self.locate(home, u"zotero.sqlite") 150 | if zoteroPath == None: 151 | QMessageBox.information(self, u"Unable to find Zotero", 152 | u"Unable to find Zotero. Please specify the Zotero folder manually.") 153 | else: 154 | self.ui.lineEditZoteroPath.setText(zoteroPath) 155 | self.ui.labelLocatePath.hide() 156 | 157 | def zoteroPathBrowse(self): 158 | 159 | """Select the Zotero folder manually""" 160 | 161 | path = QFileDialog.getExistingDirectory(self, u"Locate Zotero folder") 162 | if path != u"": 163 | self.setZoteroPath(path) 164 | -------------------------------------------------------------------------------- /libqnotero/qnotero.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of qnotero. 5 | 6 | qnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | qnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with qnotero. If not, see . 18 | """ 19 | 20 | import sys 21 | import os 22 | import subprocess 23 | from libqnotero.qt.QtGui import QMainWindow, QListWidgetItem, QLabel, \ 24 | QDesktopWidget, QMessageBox 25 | from libqnotero.qt.QtCore import QSettings, QSize, QCoreApplication 26 | from libqnotero.qt import uic 27 | from libqnotero.sysTray import SysTray 28 | from libqnotero.config import saveConfig, restoreConfig, setConfig, getConfig 29 | from libqnotero.qnoteroItemDelegate import QnoteroItemDelegate 30 | from libqnotero.qnoteroItem import QnoteroItem 31 | from libqnotero.uiloader import UiLoader 32 | from libzotero.libzotero import LibZotero 33 | 34 | class Qnotero(QMainWindow, UiLoader): 35 | 36 | """The main class of the Qnotero GUI""" 37 | 38 | version = '2.0.0' 39 | 40 | def __init__(self, systray=True, debug=False, reset=False, parent=None): 41 | 42 | """ 43 | Constructor. 44 | 45 | Keyword arguments: 46 | systray -- Enables the system tray icon (default=True) 47 | debug -- Enable debugging output (default=False) 48 | reset -- Reset preferences (default=False) 49 | parent -- Parent QWidget (default=None) 50 | """ 51 | 52 | QMainWindow.__init__(self, parent) 53 | self.loadUi('qnotero') 54 | if not reset: 55 | self.restoreState() 56 | self.debug = debug 57 | self.reInit() 58 | self.noResults() 59 | if systray: 60 | self.sysTray = SysTray(self) 61 | self.sysTray.show() 62 | self.minimizeOnClose = True 63 | else: 64 | self.minimizeOnClose = False 65 | if getConfig(u"firstRun") or not os.path.exists(getConfig('zoteroPath')): 66 | self.preferences(firstRun=True) 67 | if getConfig(u"autoUpdateCheck"): 68 | self.updateCheck() 69 | 70 | def close(self): 71 | 72 | """Exits the program.""" 73 | 74 | self.minimizeOnClose = False 75 | QMainWindow.close(self) 76 | 77 | def closeEvent(self, e): 78 | 79 | """ 80 | Close or minimze to tray, depending on when the function is called 81 | 82 | Arguments: 83 | e -- a QCloseEvent 84 | """ 85 | 86 | if self.minimizeOnClose: 87 | self.popDown() 88 | e.ignore() 89 | else: 90 | e.accept() 91 | if self.listener != None: 92 | self.listener.alive = False 93 | sys.exit() 94 | 95 | def hideNoteHint(self): 96 | 97 | """Hide the note available message""" 98 | 99 | self.ui.labelNoteAvailable.hide() 100 | 101 | def leaveEvent(self, e): 102 | 103 | """Hide the Window when the mouse is lost""" 104 | 105 | self.popDown() 106 | 107 | def openNote(self): 108 | 109 | """Open the active note""" 110 | 111 | self.activeNote.open() 112 | 113 | def noResults(self, query=None): 114 | 115 | """ 116 | Displays the no results message 117 | 118 | Keyword arguments: 119 | query -- a query (default=None) 120 | """ 121 | 122 | if query != None: 123 | self.showResultMsg(u"No results for %s" % query) 124 | else: 125 | self.showResultMsg(u"Please enter a search term") 126 | 127 | def popDown(self): 128 | 129 | """Minimize to the tray""" 130 | 131 | if self.minimizeOnClose: 132 | self.hide() 133 | else: 134 | self.close() 135 | 136 | def popUp(self): 137 | 138 | """Popup from the tray""" 139 | 140 | # Reposition the window 141 | r = QDesktopWidget().availableGeometry() 142 | s = self.size() 143 | pos = getConfig(u"pos") 144 | if pos == u"Top right": 145 | x = r.left() + r.width()-s.width() 146 | y = r.top() 147 | elif pos == u"Top left": 148 | x = r.left() 149 | y = r.top() 150 | elif pos == u"Bottom right": 151 | x = r.left() + r.width()-s.width() 152 | y = r.top() + r.height()-s.height() 153 | elif pos == u"Bottom left": 154 | x = r.left() 155 | y = r.top() + r.height()-s.height() 156 | else: 157 | x = r.left() + r.width()/2 - s.width()/2 158 | y = r.top() + r.height()/2 - s.height()/2 159 | self.move(x, y) 160 | 161 | # Show it 162 | self.show() 163 | QCoreApplication.processEvents() 164 | self.raise_() 165 | self.activateWindow() 166 | 167 | # Focus the search box 168 | self.ui.lineEditQuery.selectAll() 169 | self.ui.lineEditQuery.setFocus() 170 | 171 | def preferences(self, firstRun=False): 172 | 173 | """ 174 | Show the preferences dialog 175 | 176 | Keyword arguments: 177 | firstRun -- indicates if the first run message should be shown 178 | (default=False) 179 | """ 180 | 181 | from libqnotero.preferences import Preferences 182 | Preferences(self, firstRun=firstRun).exec_() 183 | 184 | def previewNote(self, note): 185 | 186 | """ 187 | Show the note preview 188 | 189 | Arguments: 190 | note -- the Note to preview 191 | """ 192 | 193 | self.activeNote = note 194 | self.ui.labelNote.setText(note.preview) 195 | self.hideNoteHint() 196 | self.ui.widgetNote.show() 197 | self.ui.listWidgetResults.hide() 198 | 199 | def reInit(self): 200 | 201 | """Re-inits the parts of the GUI that can be changed at runtime.""" 202 | 203 | self.setTheme() 204 | self.setupUi() 205 | self.noteProvider = [] 206 | if getConfig(u'noteProvider') == u'gnote': 207 | from libzotero._noteProvider.gnoteProvider import GnoteProvider 208 | print(u"qnotero.reInit(): using GnoteProvider") 209 | self.noteProvider = GnoteProvider(self) 210 | self.zotero = LibZotero(getConfig(u"zoteroPath"), self.noteProvider) 211 | if hasattr(self, u"sysTray"): 212 | self.sysTray.setIcon(self.theme.icon(u"qnotero")) 213 | 214 | def restoreState(self): 215 | 216 | """Restore the settings""" 217 | 218 | settings = QSettings(u"cogscinl", u"qnotero") 219 | settings.beginGroup(u"Qnotero"); 220 | restoreConfig(settings) 221 | settings.endGroup() 222 | 223 | def runResult(self, listWidgetItem): 224 | 225 | """Handle clicks on a result""" 226 | 227 | if listWidgetItem.zoteroItem.fulltext == None: 228 | return 229 | pdf = listWidgetItem.zoteroItem.fulltext 230 | if os.name == 'nt': 231 | os.startfile(pdf) 232 | else: 233 | # For some reason, the file must be encoded with latin-1, despite 234 | # the fact that it's a utf-8 encoded database and filesystem! 235 | reader = getConfig('pdfReader').encode(sys.getfilesystemencoding()) 236 | pid = subprocess.Popen([reader, pdf]) 237 | self.popDown() 238 | 239 | def saveState(self): 240 | 241 | """Save the settings""" 242 | 243 | settings = QSettings(u"cogscinl", u"qnotero") 244 | settings.beginGroup(u"Qnotero") 245 | saveConfig(settings) 246 | settings.endGroup() 247 | 248 | def search(self, setFocus=False): 249 | 250 | """ 251 | Execute a search 252 | 253 | Keyword arguments: 254 | setFocus -- indicates whether the listWidgetResults needs to receive 255 | focus (default=False) 256 | """ 257 | 258 | self.ui.labelNoteAvailable.hide() 259 | self.ui.widgetNote.hide() 260 | self.ui.listWidgetResults.show() 261 | self.ui.listWidgetResults.clear() 262 | self.ui.lineEditQuery.needUpdate = False 263 | self.ui.lineEditQuery.timer.stop() 264 | query = self.ui.lineEditQuery.text() 265 | if len(query) < getConfig(u"minQueryLength"): 266 | self.noResults() 267 | return 268 | zoteroItemList = self.zotero.search(query) 269 | if len(zoteroItemList) == 0: 270 | self.noResults(query) 271 | return 272 | self.showResultMsg(u"%d results for %s" % (len(zoteroItemList), query)) 273 | for zoteroItem in zoteroItemList: 274 | qnoteroItem = QnoteroItem(self, zoteroItem, \ 275 | self.ui.listWidgetResults) 276 | self.ui.listWidgetResults.addItem(qnoteroItem) 277 | if setFocus: 278 | self.ui.listWidgetResults.setFocus() 279 | 280 | def setSize(self, size): 281 | 282 | """ 283 | Set the window size 284 | 285 | Arguments: 286 | size -- a QSize 287 | """ 288 | 289 | self.setMinimumSize(size) 290 | self.setMaximumSize(size) 291 | 292 | def setTheme(self): 293 | 294 | """Load a theme""" 295 | 296 | theme = getConfig(u'theme') 297 | mod = __import__(u'libqnotero._themes.%s' % theme.lower(), fromlist= \ 298 | [u'dummy']) 299 | cls = getattr(mod, theme.capitalize()) 300 | self.theme = cls(self) 301 | 302 | def setupUi(self): 303 | 304 | """Setup the GUI""" 305 | 306 | self.ui.pushButtonSearch.setIcon(self.theme.icon(u"search")) 307 | self.ui.pushButtonSearch.clicked.connect(self.search) 308 | self.ui.lineEditQuery.qnotero = self 309 | self.ui.listWidgetResults.qnotero = self 310 | self.ui.listWidgetResults.setItemDelegate(QnoteroItemDelegate(self)) 311 | self.ui.listWidgetResults.itemActivated.connect(self.runResult) 312 | self.ui.widgetNote.hide() 313 | self.ui.labelNoteAvailable.hide() 314 | self.ui.pushButtonOpenNote.clicked.connect(self.openNote) 315 | 316 | def showNoteHint(self): 317 | 318 | """Indicate that a note is available""" 319 | 320 | self.ui.labelNoteAvailable.show() 321 | 322 | def showResultMsg(self, msg): 323 | 324 | """ 325 | Shows a status message. 326 | 327 | Arguments: 328 | msg -- A message. 329 | """ 330 | 331 | self.ui.labelResultMsg.setText(u"%s" % msg) 332 | 333 | def updateCheck(self): 334 | 335 | """Checks for updates if update checking is on.""" 336 | 337 | if not getConfig(u"autoUpdateCheck"): 338 | return True 339 | import urllib.request 340 | from distutils.version import LooseVersion 341 | print(u"qnotero.updateCheck(): opening %s" % getConfig(u"updateUrl")) 342 | try: 343 | fd = urllib.request.urlopen(getConfig(u"updateUrl")) 344 | mrv = fd.read().decode('utf-8').strip() 345 | except: 346 | print('qnotero.updateCheck(): failed to check for update') 347 | return 348 | print("qnotero.updateCheck(): most recent = %s, current = %s" \ 349 | % (mrv, self.version)) 350 | if LooseVersion(mrv) > LooseVersion(self.version): 351 | QMessageBox.information(self, 'Update found', 352 | ('A new version of Qnotero is available! Please visit ' 353 | 'http://www.cogsci.nl/qnotero for more information.')) 354 | -------------------------------------------------------------------------------- /libqnotero/qnoteroException.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | class QnoteroException(Exception): 19 | 20 | """A simple custom exception""" 21 | 22 | def __init__(self, value): 23 | 24 | self.value = value 25 | 26 | def __str__(self): 27 | 28 | return str(self.value) 29 | -------------------------------------------------------------------------------- /libqnotero/qnoteroItem.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | from libqnotero.qt.QtGui import QListWidgetItem 19 | 20 | class QnoteroItem(QListWidgetItem): 21 | 22 | """A single Qnotero result""" 23 | 24 | def __init__(self, qnotero, zoteroItem, parent=None): 25 | 26 | """ 27 | Constructor 28 | 29 | Arguments: 30 | qnotero -- a Qnotero instance 31 | zoteroItem -- a ZoteroItem 32 | 33 | Keyword arguments: 34 | parent -- a parent QWidget (default=None) 35 | """ 36 | 37 | QListWidgetItem.__init__(self, zoteroItem.hashKey(), parent) 38 | self.qnotero = qnotero 39 | self.zoteroItem = zoteroItem 40 | -------------------------------------------------------------------------------- /libqnotero/qnoteroItemDelegate.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of qnotero. 5 | 6 | qnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | qnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with qnotero. If not, see . 18 | """ 19 | 20 | from libqnotero.qt.QtGui import QStyledItemDelegate, QApplication, QStyle 21 | from libqnotero.qt.QtGui import QPen, QPalette, QFont, QFontMetrics 22 | from libqnotero.qt.QtCore import Qt, QRect, QSize 23 | from libqnotero.config import getConfig 24 | from libzotero.zotero_item import cache as zoteroCache 25 | 26 | class QnoteroItemDelegate(QStyledItemDelegate): 27 | 28 | """Draws pretty result items""" 29 | 30 | def __init__(self, qnotero): 31 | 32 | """ 33 | Constructor 34 | 35 | Arguments: 36 | qnotero -- a Qnotero instance 37 | """ 38 | 39 | QStyledItemDelegate.__init__(self, qnotero) 40 | self.qnotero = qnotero 41 | self.boldFont = QFont() 42 | self.boldFont.setBold(True) 43 | self.regularFont = QFont() 44 | self.italicFont = QFont() 45 | self.italicFont.setItalic(True) 46 | self.tagFont = QFont() 47 | self.tagFont.setBold(True) 48 | self.tagFont.setPointSize(self.boldFont.pointSize() - 2) 49 | self.dy = QFontMetrics(self.boldFont) \ 50 | .size(Qt.TextSingleLine, u"Dummy").height() \ 51 | *self.qnotero.theme.lineHeight() 52 | self.margin = 0.5*self.dy 53 | self._margin = 0.1*self.dy 54 | self.height = 5*self.dy+self._margin 55 | self.noPdfPixmap = self.qnotero.theme.pixmap(u"nopdf") 56 | self.pdfPixmap = self.qnotero.theme.pixmap(u"pdf") 57 | self.aboutPixmap = self.qnotero.theme.pixmap(u"about") 58 | self.notePixmap = self.qnotero.theme.pixmap(u"note") 59 | self.pixmapSize = self.pdfPixmap.height()+0.5*self.dy 60 | self.roundness = self.qnotero.theme.roundness() 61 | 62 | def sizeHint(self, option, index): 63 | 64 | """ 65 | Suggest a size for the widget 66 | 67 | Arguments: 68 | option -- a QStyleOptionView 69 | index -- a QModelIndex 70 | 71 | Returns: 72 | A QSize 73 | """ 74 | 75 | return QSize(0, self.height) 76 | 77 | def paint(self, painter, option, index): 78 | 79 | """ 80 | Draws the widget 81 | 82 | Arguments: 83 | painter -- a QPainter 84 | option -- a QStyleOptionView 85 | index -- a QModelIndex 86 | """ 87 | 88 | # Retrieve the data 89 | model = index.model() 90 | record = model.data(index) 91 | text = record 92 | zoteroItem = zoteroCache[text] 93 | l = zoteroItem.full_format().split(u"\n") 94 | if zoteroItem.fulltext == None: 95 | pixmap = self.noPdfPixmap 96 | else: 97 | pixmap = self.pdfPixmap 98 | 99 | # Choose the colors 100 | self.palette = self.qnotero.ui.listWidgetResults.palette() 101 | if option.state & QStyle.State_MouseOver: 102 | background = self.palette.Highlight 103 | foreground = self.palette.HighlightedText 104 | _note = zoteroItem.get_note() 105 | if _note != None: 106 | self.qnotero.showNoteHint() 107 | else: 108 | self.qnotero.hideNoteHint() 109 | 110 | elif option.state & QStyle.State_Selected: 111 | background = self.palette.Dark 112 | foreground = self.palette.WindowText 113 | else: 114 | background = self.palette.Base 115 | foreground = self.palette.WindowText 116 | 117 | # Draw the frame 118 | _rect = option.rect.adjusted(self._margin, self._margin, \ 119 | -2*self._margin, -self._margin) 120 | pen = painter.pen() 121 | pen.setColor(self.palette.color(background)) 122 | painter.setPen(pen) 123 | painter.setBrush(self.palette.brush(background)) 124 | painter.drawRoundedRect(_rect, self.roundness, self.roundness) 125 | font = painter.font 126 | pen = painter.pen() 127 | pen.setColor(self.palette.color(foreground)) 128 | painter.setPen(pen) 129 | 130 | # Draw icon 131 | _rect = QRect(option.rect) 132 | _rect.moveBottom(_rect.bottom() + 0.5*self.dy) 133 | _rect.moveLeft(_rect.left() + 0.5*self.dy) 134 | _rect.setHeight(self.pixmapSize) 135 | _rect.setWidth(self.pixmapSize) 136 | painter.drawPixmap(_rect, pixmap) 137 | 138 | # Draw the text 139 | _rect = option.rect.adjusted(self.pixmapSize+self.dy, 0.5*self.dy, \ 140 | -self.dy, 0) 141 | 142 | f = [self.tagFont, self.italicFont, self.regularFont, \ 143 | self.boldFont] 144 | l.reverse() 145 | while len(l) > 0: 146 | s = l.pop() 147 | if len(f) > 0: 148 | painter.setFont(f.pop()) 149 | painter.drawText(_rect, Qt.AlignLeft, s) 150 | _rect = _rect.adjusted(0, self.dy, 0, 0) 151 | 152 | 153 | -------------------------------------------------------------------------------- /libqnotero/qnoteroQuery.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | from libqnotero.qt.QtGui import QLineEdit 19 | from libqnotero.qt.QtCore import Qt, QTimer 20 | from libqnotero.config import getConfig 21 | 22 | class QnoteroQuery(QLineEdit): 23 | 24 | """The search input box""" 25 | 26 | def __init__(self, qnotero): 27 | 28 | """ 29 | Constructor 30 | 31 | Arguments: 32 | qnotero -- a Qnotero instance 33 | """ 34 | 35 | QLineEdit.__init__(self, qnotero) 36 | self.qnotero = qnotero 37 | self.timer = QTimer(self) 38 | self.needUpdate = True 39 | self.textChanged.connect(self._textChanged) 40 | 41 | def keyPressEvent(self, e): 42 | 43 | """ 44 | Handle key presses 45 | 46 | Arguments: 47 | e -- a QKeyEvent 48 | """ 49 | 50 | if e.key() == Qt.Key_Return: 51 | self.qnotero.search(setFocus=False) 52 | return 53 | if e.key() == Qt.Key_Down: 54 | if self.needUpdate: 55 | self.qnotero.search(setFocus=True) 56 | elif self.qnotero.ui.listWidgetResults.count() > 0: 57 | self.qnotero.ui.listWidgetResults.setFocus() 58 | self.qnotero.ui.listWidgetResults.setCurrentItem( \ 59 | self.qnotero.ui.listWidgetResults.item(0)) 60 | return 61 | 62 | QLineEdit.keyPressEvent(self, e) 63 | self.timer.stop() 64 | self.timer = QTimer(self) 65 | self.timer.setSingleShot(True) 66 | self.timer.setInterval(getConfig("autoFire")) 67 | self.timer.timeout.connect(self.search) 68 | self.timer.start() 69 | 70 | def search(self): 71 | 72 | """Perform a search without losing focus""" 73 | 74 | self.qnotero.search(setFocus=False) 75 | 76 | def _textChanged(self): 77 | 78 | """Set the needUpdate flag""" 79 | 80 | self.needUpdate = True 81 | -------------------------------------------------------------------------------- /libqnotero/qnoteroResults.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | from libqnotero.qt.QtGui import QListWidget 19 | from libqnotero.qt.QtGui import QDrag 20 | from libqnotero.qt.QtCore import Qt, QUrl, QMimeData 21 | import urllib 22 | import shutil 23 | import tempfile 24 | import os.path 25 | import time 26 | 27 | class QnoteroResults(QListWidget): 28 | 29 | """The Qnotero result list""" 30 | 31 | def __init__(self, qnotero): 32 | 33 | """ 34 | Constructor 35 | 36 | Arguments: 37 | qnotero -- a Qnotero instance 38 | """ 39 | 40 | QListWidget.__init__(self, qnotero) 41 | self.setMouseTracking(True) 42 | 43 | def mousePressEvent(self, e): 44 | 45 | """ 46 | Start a drag operation 47 | 48 | Arguments: 49 | e -- a QMouseEvent 50 | """ 51 | 52 | if e.button() == Qt.RightButton: 53 | item = self.itemAt(e.pos()) 54 | if item == None: 55 | return 56 | note = item.zoteroItem.get_note() 57 | if note != None: 58 | self.qnotero.previewNote(note) 59 | return 60 | 61 | QListWidget.mousePressEvent(self, e) 62 | qnoteroItem = self.currentItem() 63 | if qnoteroItem == None: 64 | return 65 | if not hasattr(qnoteroItem, "zoteroItem"): 66 | return 67 | zoteroItem = qnoteroItem.zoteroItem 68 | if zoteroItem.fulltext == None: 69 | return 70 | 71 | path = zoteroItem.fulltext.encode("latin-1") 72 | tmpName = '%s.pdf' % zoteroItem.filename_format() 73 | tmpFile = os.path.join(tempfile.gettempdir(), tmpName) 74 | suffix = 1 75 | while os.path.exists(tmpFile): 76 | tmpName = '%s-%d.pdf' % (zoteroItem.filename_format(), suffix) 77 | tmpFile = os.path.join(tempfile.gettempdir(), tmpName) 78 | suffix += 1 79 | try: 80 | shutil.copy(path, tmpFile) 81 | except: 82 | print("qnoteroResults.mousePressEvent(): failed to copy file, sorry...") 83 | return 84 | 85 | print("qnoteroResults.mousePressEvent(): prepare to copy %s" % path) 86 | print("qnoteroResults.mousePressEvent(): prepare to copy (tmp) %s" \ 87 | % tmpFile) 88 | mimeData = QMimeData() 89 | mimeData.setUrls([QUrl.fromLocalFile(tmpFile)]) 90 | mimeData.setData("text/plain", tmpFile) 91 | drag = QDrag(self) 92 | drag.setMimeData(mimeData) 93 | drag.exec_(Qt.CopyAction) 94 | 95 | def keyPressEvent(self, e): 96 | 97 | """ 98 | Handle key presses 99 | 100 | Arguments: 101 | e -- a QKeyEvent 102 | """ 103 | 104 | if (e.key() == Qt.Key_Up and self.currentRow() == 0) \ 105 | or (e.key() == Qt.Key_F and Qt.ControlModifier & e.modifiers()): 106 | self.qnotero.ui.lineEditQuery.selectAll() 107 | self.qnotero.ui.lineEditQuery.setFocus() 108 | return 109 | QListWidget.keyPressEvent(self, e) 110 | -------------------------------------------------------------------------------- /libqnotero/qt/QtCore.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of OpenSesame. 5 | 6 | OpenSesame is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OpenSesame is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OpenSesame. If not, see . 18 | """ 19 | 20 | from libqnotero import qt 21 | if qt.pyqt == 5: 22 | from PyQt5.QtCore import * 23 | else: 24 | from PyQt4.QtCore import * 25 | -------------------------------------------------------------------------------- /libqnotero/qt/QtGui.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of OpenSesame. 5 | 6 | OpenSesame is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OpenSesame is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OpenSesame. If not, see . 18 | """ 19 | 20 | from libqnotero import qt 21 | if qt.pyqt == 5: 22 | from PyQt5.QtGui import * 23 | from PyQt5.QtWidgets import * 24 | else: 25 | from PyQt4.QtGui import * 26 | -------------------------------------------------------------------------------- /libqnotero/qt/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of OpenSesame. 5 | 6 | OpenSesame is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OpenSesame is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OpenSesame. If not, see . 18 | """ 19 | 20 | import sys 21 | if '--qt5' in sys.argv: 22 | try: 23 | import PyQt5 24 | pyqt = 5 25 | except: 26 | pyqt = 4 27 | else: 28 | pyqt = 4 29 | 30 | if pyqt == 4: 31 | import sip 32 | sip.setapi(u'QString', 2) 33 | sip.setapi(u'QVariant', 2) 34 | -------------------------------------------------------------------------------- /libqnotero/qt/uic.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of OpenSesame. 5 | 6 | OpenSesame is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OpenSesame is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OpenSesame. If not, see . 18 | """ 19 | 20 | from libqnotero import qt 21 | if qt.pyqt == 5: 22 | from PyQt5.uic import * 23 | else: 24 | from PyQt4.uic import * 25 | -------------------------------------------------------------------------------- /libqnotero/sysTray.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of qnotero. 3 | 4 | qnotero is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | qnotero is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with qnotero. If not, see . 16 | """ 17 | 18 | from libqnotero.qt.QtGui import QSystemTrayIcon, QMenu 19 | from libqnotero.qt.QtCore import Qt, QObject, pyqtSignal 20 | from libqnotero.config import getConfig 21 | 22 | class SysTray(QSystemTrayIcon): 23 | 24 | """The Qnotero system tray icon""" 25 | 26 | listenerActivated = pyqtSignal() 27 | 28 | def __init__(self, qnotero): 29 | 30 | """ 31 | Constructor 32 | 33 | Arguments: 34 | qnotero -- a Qnotero instance 35 | """ 36 | 37 | QSystemTrayIcon.__init__(self, qnotero) 38 | self.qnotero = qnotero 39 | self.setIcon(self.qnotero.theme.icon("qnotero")) 40 | self.menu = QMenu() 41 | self.menu.addAction(self.qnotero.theme.icon("qnotero"), "Show", 42 | self.qnotero.popUp) 43 | self.menu.addAction(self.qnotero.theme.icon("preferences"), 44 | "Preferences", self.qnotero.preferences) 45 | self.menu.addAction(self.qnotero.theme.icon("close"), "Close", 46 | self.qnotero.close) 47 | self.setContextMenu(self.menu) 48 | self.activated.connect(self.activate) 49 | self.listenerActivated.connect(self.activate) 50 | 51 | def activate(self, reason=None): 52 | 53 | """ 54 | Handle clicks on the systray icon 55 | 56 | Keyword arguments: 57 | reason -- the reason for activation (default=None) 58 | """ 59 | 60 | 61 | if reason == QSystemTrayIcon.Context: 62 | return 63 | if self.qnotero.isVisible(): 64 | self.qnotero.popDown() 65 | else: 66 | self.qnotero.popUp() 67 | -------------------------------------------------------------------------------- /libqnotero/ui/preferences.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Preferences 4 | 5 | 6 | 7 | 0 8 | 0 9 | 473 10 | 312 11 | 12 | 13 | 14 | Preferences 15 | 16 | 17 | 18 | 19 | 20 | Zotero folder 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Qt::Horizontal 31 | 32 | 33 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 34 | 35 | 36 | 37 | 38 | 39 | 40 | Browse 41 | 42 | 43 | 44 | 45 | 46 | 47 | Auto-detect 48 | 49 | 50 | 51 | 52 | 53 | 54 | Theme 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 66 | 67 | 68 | 69 | 70 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 71 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 72 | p, li { white-space: pre-wrap; } 73 | </style></head><body style=" font-family:'Ubuntu'; font-size:10pt; font-weight:400; font-style:normal;"> 74 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans Serif'; font-size:9pt; font-weight:600;">Qnotero [version]</span></p> 75 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans Serif'; font-size:8pt; font-style:italic;">Copyright 2011-2014 Sebastiaan Mathôt</span></p> 76 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Sans Serif'; font-size:8pt; font-style:italic;">http://www.cogsci.nl/</span></p></body></html> 77 | 78 | 79 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | TextLabel 90 | 91 | 92 | 93 | 94 | 95 | 96 | Automatically check for updates on start-up 97 | 98 | 99 | 100 | 101 | 102 | 103 | This appears to be the first time that you run Qnotero! Getting started is easy, all you need to do is locate the Zotero folder. if you don't know where the Zotero folder is located, you can use the auto-detect function (but this may take a while). 104 | 105 | 106 | Qt::PlainText 107 | 108 | 109 | true 110 | 111 | 112 | 113 | 114 | 115 | 116 | Position 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | Top right 125 | 126 | 127 | 128 | 129 | Top left 130 | 131 | 132 | 133 | 134 | Bottom right 135 | 136 | 137 | 138 | 139 | Bottom left 140 | 141 | 142 | 143 | 144 | Center 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | buttonBox 155 | accepted() 156 | Preferences 157 | accept() 158 | 159 | 160 | 248 161 | 254 162 | 163 | 164 | 157 165 | 274 166 | 167 | 168 | 169 | 170 | buttonBox 171 | rejected() 172 | Preferences 173 | reject() 174 | 175 | 176 | 316 177 | 260 178 | 179 | 180 | 286 181 | 274 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /libqnotero/ui/qnotero.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Qnotero 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 577 11 | 12 | 13 | 14 | 15 | 400 16 | 0 17 | 18 | 19 | 20 | Qnotero 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 0 29 | 0 30 | 31 | 32 | 33 | 34 | 0 35 | 36 | 37 | 38 | 39 | 40 | 0 41 | 32 42 | 43 | 44 | 45 | false 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 32 57 | 32 58 | 59 | 60 | 61 | true 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 0 73 | 0 74 | 75 | 76 | 77 | TextLabel 78 | 79 | 80 | 81 | 82 | 83 | 84 | QFrame::NoFrame 85 | 86 | 87 | true 88 | 89 | 90 | QAbstractItemView::DragOnly 91 | 92 | 93 | Qt::CopyAction 94 | 95 | 96 | 97 | 98 | 99 | 100 | Right click on item to view note 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 0 109 | 110 | 111 | 112 | 113 | 114 | 0 115 | 0 116 | 117 | 118 | 119 | TextLabel 120 | 121 | 122 | Qt::RichText 123 | 124 | 125 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 126 | 127 | 128 | true 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 0 137 | 0 138 | 139 | 140 | 141 | 142 | 0 143 | 144 | 145 | 146 | 147 | Open note 148 | 149 | 150 | 151 | 152 | 153 | 154 | Return 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | QnoteroResults 170 | QListWidget 171 |
libqnotero/qnoteroResults.h
172 |
173 | 174 | QnoteroQuery 175 | QLineEdit 176 |
libqnotero/qnoteroQuery.h
177 |
178 |
179 | 180 | 181 | 182 | pushButtonReturnFromNote 183 | clicked() 184 | listWidgetResults 185 | show() 186 | 187 | 188 | 296 189 | 555 190 | 191 | 192 | 199 193 | 302 194 | 195 | 196 | 197 | 198 | pushButtonReturnFromNote 199 | clicked() 200 | widgetNote 201 | hide() 202 | 203 | 204 | 296 205 | 555 206 | 207 | 208 | 199 209 | 544 210 | 211 | 212 | 213 | 214 |
215 | -------------------------------------------------------------------------------- /libqnotero/uiloader.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of qnotero. 5 | 6 | qnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | qnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with qnotero. If not, see . 18 | """ 19 | 20 | import os 21 | from libqnotero.qt import QtCore, QtGui, uic 22 | 23 | class UiLoader(QtCore.QObject): 24 | 25 | """ 26 | desc: 27 | A base object from classes that dynamically load a UI file. 28 | """ 29 | 30 | def loadUi(self, ui): 31 | 32 | """ 33 | desc: 34 | Dynamically loads a UI file. 35 | 36 | arguments: 37 | ui: 38 | desc: The name of a UI file, which should match. 39 | libqnotero/ui/[name].ui 40 | type: str 41 | """ 42 | 43 | path = os.path.dirname(__file__) 44 | # If we are running from a frozen state (i.e. packaged by py2exe), we 45 | # need to find the UI files relative to the executable directory, 46 | # because the modules are packaged into library.zip. 47 | if os.name == 'nt': 48 | import imp 49 | import sys 50 | if (hasattr(sys, 'frozen') or hasattr(sys, 'importers') or \ 51 | imp.is_frozen('__main__')): 52 | path = os.path.join(os.path.dirname(sys.executable), 53 | 'libqnotero') 54 | uiPath = os.path.join(path, 'ui', '%s.ui' % ui) 55 | self.ui = uic.loadUi(uiPath, self) 56 | -------------------------------------------------------------------------------- /libzotero/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/libzotero/__init__.py -------------------------------------------------------------------------------- /libzotero/_noteProvider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/libzotero/_noteProvider/__init__.py -------------------------------------------------------------------------------- /libzotero/_noteProvider/gnoteProvider.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of Gnotero. 5 | 6 | Gnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | Gnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with Gnotero. If not, see . 18 | """ 19 | 20 | import os 21 | import os.path 22 | import re 23 | import subprocess 24 | try: 25 | import Levenshtein 26 | except: 27 | print("libzotero._noteProvider.gnoteProvider: failed to import Levenshtein") 28 | 29 | class GnoteProvider(object): 30 | 31 | """ 32 | libgnote provides an interface to Gnote and is a replacement 33 | for the deprecated gnote.py module 34 | """ 35 | 36 | def __init__(self, qnotero): 37 | 38 | if os.name != "posix": 39 | self.path = None 40 | return 41 | 42 | home_folder = os.environ["HOME"] 43 | 44 | # Determine the location of the gnote notes 45 | if os.path.exists(os.path.join(home_folder, ".local/share/gnote")): 46 | self.path = os.path.join(home_folder, ".local/share/gnote") 47 | print("libgnote.__init__(): gnote path is %s" % self.path) 48 | 49 | elif os.path.exists(os.path.join(home_folder, ".gnote")): 50 | self.path = os.path.join(home_folder, ".gnote") 51 | print("libgnote.__init__(): gnote path is %s" % self.path) 52 | 53 | else: 54 | print("libgnote.__init__(): failed to locate Gnote") 55 | self.path = None 56 | 57 | def search(self, item): 58 | 59 | """ 60 | Search gnote for a note matching an author and a year 61 | """ 62 | 63 | if self.path == None: 64 | return None 65 | 66 | # Compile some regexps to search for the note, 67 | # extract the note and remove xml tags 68 | p = re.compile(r"%s.*\(%s\)" % (item.authors[0], item.date), re.IGNORECASE) 69 | get_note = re.compile(r"%s.*?\(%s\).*?" % (item.authors[0], item.date), re.IGNORECASE and re.DOTALL) 70 | strip_p = re.compile('<.*?>') 71 | matches = [] 72 | 73 | # Walk through all notes 74 | for fnote in os.listdir(self.path): 75 | 76 | if os.path.splitext(fnote)[1] == ".note": 77 | 78 | note_path = os.path.join(self.path, fnote) 79 | 80 | # Read the contents of the note 81 | f = open(note_path, "r") 82 | s = f.read() 83 | 84 | # Search the note according to the regular expression 85 | m = p.search(s) 86 | 87 | # If a contains a match 88 | if m != None: 89 | 90 | # Extract the matching content 91 | s = s[m.span()[0]:] 92 | m = get_note.search(s[:s.find("")]) 93 | if m != None: 94 | start, end = m.span() 95 | else: 96 | start = 0 97 | end = len(s) 98 | pango = s[start:end] 99 | 100 | # Remove tags, strip whitespaces and highlight the 101 | # search terms 102 | pango = strip_p.sub("", pango)[:1024].strip() 103 | for s in (item.authors[0], item.date): 104 | try: 105 | pango = pango.replace("%s" % s, "%s" % s) 106 | except: 107 | pass 108 | 109 | # Add this result to the list 110 | matches.append(GnoteNote(pango, "gnote --open-note=%s" % note_path)) 111 | 112 | if len(matches) == 0: 113 | return None 114 | 115 | elif len(matches) == 1: 116 | print("libgnote.search(): 1 note found matching %s (%s)" % (item.authors[0], item.date)) 117 | 118 | else: 119 | 120 | # If there are multiple matches, try to figure out which note is most likely 121 | # the actual note 122 | 123 | print("libgnote.search(): %d matches found, sorting by relevance" % len(matches)) 124 | matches.sort(key=lambda m: m.matchScore(item)) 125 | 126 | return matches[0] 127 | 128 | class GnoteNote: 129 | 130 | """ 131 | A class containing a note 132 | """ 133 | 134 | def __init__(self, preview, cmd): 135 | 136 | self.preview = preview 137 | self.cmd = cmd 138 | 139 | def matchScore(self, item): 140 | 141 | """ 142 | Determines the best match 143 | """ 144 | 145 | first_line = self.preview.split("\n")[0].replace("&", "&") 146 | match = item.simple_format() + " " + item.format_publication() 147 | score = Levenshtein.distance(first_line, match) 148 | 149 | return score 150 | 151 | def open(self): 152 | 153 | """Open the note in Gnote""" 154 | 155 | subprocess.call(self.cmd.split()) 156 | 157 | -------------------------------------------------------------------------------- /libzotero/libzotero.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of Gnotero. 5 | 6 | Gnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | Gnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with Gnotero. If not, see . 18 | """ 19 | 20 | import sqlite3 21 | import os 22 | import os.path 23 | import sys 24 | import shutil 25 | import sys 26 | import time 27 | from libzotero.zotero_item import zoteroItem as zotero_item 28 | 29 | class LibZotero(object): 30 | 31 | """ 32 | Libzotero provides access to the zotero database. 33 | This is an object oriented reimplementation of the 34 | original zoterotools. 35 | """ 36 | 37 | attachment_query = u""" 38 | select items.itemID, itemAttachments.path, itemAttachments.itemID 39 | from items, itemAttachments 40 | where items.itemID = itemAttachments.sourceItemID 41 | """ 42 | 43 | info_query = u""" 44 | select items.itemID, fields.fieldName, itemDataValues.value, items.key 45 | from items, itemData, fields, itemDataValues 46 | where 47 | items.itemID = itemData.itemID 48 | and itemData.fieldID = fields.fieldID 49 | and itemData.valueID = itemDataValues.valueID 50 | and (fields.fieldName = "date" 51 | or fields.fieldName = "publicationTitle" 52 | or fields.fieldName = "volume" 53 | or fields.fieldName = "issue" 54 | or fields.fieldName = "title") 55 | """ 56 | 57 | author_query = u""" 58 | select items.itemID, creatorData.lastName 59 | from items, itemCreators, creators, creatorData, creatorTypes 60 | where 61 | items.itemID = itemCreators.itemID 62 | and itemCreators.creatorID = creators.creatorID 63 | and creators.creatorDataID = creatorData.creatorDataID 64 | and itemCreators.creatorTypeID = creatorTypes.creatorTypeID 65 | and creatorTypes.creatorType != "editor" 66 | order by itemCreators.orderIndex 67 | """ 68 | 69 | collection_query = u""" 70 | select items.itemID, collections.collectionName 71 | from items, collections, collectionItems 72 | where 73 | items.itemID = collectionItems.itemID 74 | and collections.collectionID = collectionItems.collectionID 75 | order by collections.collectionName != "To Read", 76 | collections.collectionName 77 | """ 78 | 79 | tag_query = u""" 80 | select items.itemID, tags.name 81 | from items, tags, itemTags 82 | where 83 | items.itemID = itemTags.itemID 84 | and tags.tagID = itemTags.tagID 85 | """ 86 | 87 | deleted_query = u"select itemID from deletedItems" 88 | 89 | def __init__(self, zotero_path, noteProvider=None): 90 | 91 | """ 92 | Intialize libzotero. 93 | 94 | Arguments: 95 | zotero_path -- A string to the Zotero folder. 96 | 97 | Keyword arguments: 98 | noteProvider -- A noteProvider object. (default=None) 99 | """ 100 | 101 | assert(isinstance(zotero_path, str)) 102 | print(u"libzotero.__init__(): zotero_path = %s" % bytes(zotero_path, 103 | 'utf-8')) 104 | # Set paths 105 | self.zotero_path = zotero_path 106 | self.storage_path = os.path.join(self.zotero_path, u"storage") 107 | self.zotero_database = os.path.join(self.zotero_path, u"zotero.sqlite") 108 | self.noteProvider = noteProvider 109 | if os.name == u"nt": 110 | home_folder = os.environ[u"USERPROFILE"] 111 | elif os.name == u"posix": 112 | home_folder = os.environ[u"HOME"] 113 | else: 114 | print(u"libzotero.__init__(): you appear to be running an unsupported OS") 115 | 116 | self.gnotero_database = os.path.join(home_folder, u".gnotero.sqlite") 117 | # Remember search results so results speed up over time 118 | self.search_cache = {} 119 | # Check whether verbosity is turned on 120 | self.verbose = "-v" in sys.argv 121 | # These dates are treated as special and are not parsed into a year 122 | # representation 123 | self.special_dates = u"in press", u"submitted", u"in preparation", \ 124 | u"unpublished" 125 | # These extensions are recognized as fulltext attachments 126 | self.attachment_ext = u".pdf", u".epub" 127 | 128 | self.index = {} 129 | self.collection_index = [] 130 | self.tag_index = [] 131 | self.last_update = None 132 | 133 | # The notry parameter can be used to show errors which would 134 | # otherwise be obscured by the try clause 135 | if "--notry" in sys.argv: 136 | self.search(u"dummy") 137 | 138 | # Start by updating the database 139 | try: 140 | self.search(u"dummy") 141 | self.error = False 142 | except Exception as e: 143 | print(e) 144 | self.error = True 145 | 146 | def update(self, force=False): 147 | 148 | """ 149 | Checks if the local copy of the zotero database is up to date. If not, 150 | the data is also indexed. 151 | 152 | Arguments: 153 | force -- Indicates that the data should also be indexed, even 154 | if the local copy is up to date. (default=False) 155 | """ 156 | 157 | try: 158 | stats = os.stat(self.zotero_database) 159 | except Exception as e: 160 | print(u"libzotero.update(): %s" % e) 161 | return False 162 | 163 | # Only update if necessary 164 | if force or self.last_update == None or stats[8] > self.last_update: 165 | t = time.time() 166 | self.last_update = stats[8] 167 | self.index = {} 168 | self.collection_index = [] 169 | self.search_cache = {} 170 | # Copy the zotero database to the gnotero copy 171 | shutil.copyfile(self.zotero_database, self.gnotero_database) 172 | self.conn = sqlite3.connect(self.gnotero_database) 173 | self.cur = self.conn.cursor() 174 | # First create a list of deleted items, so we can ignore those later 175 | deleted = [] 176 | self.cur.execute(self.deleted_query) 177 | for item in self.cur.fetchall(): 178 | deleted.append(item[0]) 179 | # Retrieve information about date, publication, volume, issue and 180 | # title 181 | self.cur.execute(self.info_query) 182 | for item in self.cur.fetchall(): 183 | item_id = item[0] 184 | key = item[3] 185 | if item_id not in deleted: 186 | item_name = item[1] 187 | # Parse date fields, because we only want a year or a # 188 | # 'special' date 189 | if item_name == u"date": 190 | item_value = None 191 | for sd in self.special_dates: 192 | if sd in item[2].lower(): 193 | item_value = sd 194 | break 195 | # Dates can have months, days, and years, or just a 196 | # year, and can be split by '-' and '/' characters. 197 | if item_value == None: 198 | # Detect whether the date should be split 199 | if u'/' in item[2]: 200 | split = u'/' 201 | elif u'-' in item[3]: 202 | split = u'-' 203 | else: 204 | split = None 205 | # If not, just use the last four characters 206 | if split == None: 207 | item_value = item[2][-4:] 208 | # Else take the first slice that is four characters 209 | else: 210 | l = item[2].split(split) 211 | for i in l: 212 | if len(i) == 4: 213 | item_value = i 214 | break 215 | else: 216 | item_value = item[2] 217 | if item_id not in self.index: 218 | self.index[item_id] = zotero_item(item_id, \ 219 | noteProvider=self.noteProvider) 220 | self.index[item_id].key = key 221 | if item_name == u"publicationTitle": 222 | self.index[item_id].publication = str(item_value) 223 | elif item_name == u"date": 224 | self.index[item_id].date = item_value 225 | elif item_name == u"volume": 226 | self.index[item_id].volume = item_value 227 | elif item_name == u"issue": 228 | self.index[item_id].issue = item_value 229 | elif item_name == u"title": 230 | self.index[item_id].title = str(item_value) 231 | # Retrieve author information 232 | self.cur.execute(self.author_query) 233 | for item in self.cur.fetchall(): 234 | item_id = item[0] 235 | if item_id not in deleted: 236 | item_author = item[1].title() 237 | if item_id not in self.index: 238 | self.index[item_id] = zotero_item(item_id) 239 | self.index[item_id].authors.append(item_author) 240 | # Retrieve collection information 241 | self.cur.execute(self.collection_query) 242 | for item in self.cur.fetchall(): 243 | item_id = item[0] 244 | if item_id not in deleted: 245 | item_collection = item[1] 246 | if item_id not in self.index: 247 | self.index[item_id] = zotero_item(item_id) 248 | self.index[item_id].collections.append(item_collection) 249 | if item_collection not in self.collection_index: 250 | self.collection_index.append(item_collection) 251 | # Retrieve tag information 252 | self.cur.execute(self.tag_query) 253 | for item in self.cur.fetchall(): 254 | item_id = item[0] 255 | if item_id not in deleted: 256 | item_tag = item[1] 257 | if item_id not in self.index: 258 | self.index[item_id] = zotero_item(item_id) 259 | self.index[item_id].tags.append(item_tag) 260 | if item_tag not in self.tag_index: 261 | self.tag_index.append(item_tag) 262 | # Retrieve attachments 263 | self.cur.execute(self.attachment_query) 264 | for item in self.cur.fetchall(): 265 | item_id = item[0] 266 | if item_id not in deleted: 267 | if item[1] != None: 268 | att = item[1] 269 | # If the attachment is stored in the Zotero folder, it is preceded 270 | # by "storage:" 271 | if att[:8] == u"storage:": 272 | item_attachment = att[8:] 273 | # The item attachment appears to be encoded in 274 | # latin-1 encoding, which we don't want, so recode. 275 | item_attachment = item_attachment.encode( 276 | 'latin-1').decode('utf-8') 277 | attachment_id = item[2] 278 | if item_attachment[-4:].lower() in \ 279 | self.attachment_ext: 280 | if item_id not in self.index: 281 | self.index[item_id] = zotero_item(item_id) 282 | self.cur.execute( \ 283 | u"select items.key from items where itemID = %d" \ 284 | % attachment_id) 285 | key = self.cur.fetchone()[0] 286 | self.index[item_id].fulltext = os.path.join( \ 287 | self.storage_path, key, item_attachment) 288 | # If the attachment is linked, it is simply the full 289 | # path to the attachment 290 | else: 291 | self.index[item_id].fulltext = att 292 | self.cur.close() 293 | print(u"libzotero.update(): indexing completed in %.3fs" \ 294 | % (time.time() - t)) 295 | return True 296 | 297 | def parse_query(self, query): 298 | 299 | """ 300 | Parses a text search query into a list of tuples, which are acceptable 301 | for zotero_item.match(). 302 | 303 | Argument: 304 | query -- A search query. 305 | 306 | Returns: 307 | A list of tuples. 308 | """ 309 | 310 | # Make sure that spaces are handled correctly after 311 | # semicolons. E.g., Author: Mathot 312 | while u": " in query: 313 | query = query.replace(u": ", u":") 314 | # Parse the terms into a suitable format 315 | terms = [] 316 | # Check if the criterium is type-specified, like "author: doe" 317 | import shlex 318 | for term in query.strip().lower().split(): 319 | s = term.split(u":") 320 | if len(s) == 2: 321 | terms.append( (s[0].strip(), s[1].strip()) ) 322 | else: 323 | terms.append( (None, term.strip()) ) 324 | return terms 325 | 326 | def search(self, query): 327 | 328 | """ 329 | Searches the zotero database. 330 | 331 | Argument: 332 | query -- A search query. 333 | 334 | Returns: 335 | A list of zotero_items. 336 | """ 337 | 338 | if not self.update(): 339 | return [] 340 | if query in self.search_cache: 341 | print( \ 342 | u"libzotero.search(): retrieving results for '%s' from cache" \ 343 | % query) 344 | return self.search_cache[query] 345 | t = time.time() 346 | terms = self.parse_query(query) 347 | results = [] 348 | for item_id, item in self.index.items(): 349 | if item.match(terms): 350 | results.append(item) 351 | self.search_cache[query] = results 352 | print(u"libzotero.search(): search for '%s' completed in %.3fs" % \ 353 | (query, time.time() - t)) 354 | return results 355 | 356 | def valid_location(path): 357 | 358 | """ 359 | Checks if a given path is a valid Zotero folder, i.e., if it it contains 360 | zotero.sqlite. 361 | 362 | Arguments: 363 | path -- The path to check. 364 | 365 | Returns: 366 | True if path is a valid Zotero folder, False otherwise. 367 | """ 368 | 369 | assert(isinstance(path, str)) 370 | return os.path.exists(os.path.join(path, u"zotero.sqlite")) 371 | -------------------------------------------------------------------------------- /libzotero/zotero_item.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | """ 4 | This file is part of Gnotero. 5 | 6 | Gnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | Gnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with Gnotero. If not, see . 18 | """ 19 | 20 | term_collection = None, u"collection" 21 | term_tag = None, u"tag" 22 | term_author = None, u"author" 23 | term_date = None, u"date", u"year" 24 | term_publication = None, u"publication", u"journal" 25 | term_title = None, u"title" 26 | 27 | cache = {} 28 | 29 | class zoteroItem(object): 30 | 31 | """Represents a single zotero item.""" 32 | 33 | def __init__(self, init=None, noteProvider=None): 34 | 35 | """ 36 | Constructor. 37 | 38 | Keyword arguments: 39 | init -- A `dict` with item information, or an `int` with the 40 | item id . (default=None) 41 | noteProvider -- A noteProvider object. (default=None) 42 | """ 43 | 44 | self.gnotero_format_str = None 45 | self.simple_format_str = None 46 | self.filename_format_str = None 47 | self.collection_color = u"#000000" 48 | self.noteProvider = noteProvider 49 | self.note = -1 50 | if isinstance(init, dict): 51 | if u"item_id" in item: 52 | self.id = item[u"item_id"] 53 | else: 54 | self.id = None 55 | if u"publicationTitle" in item: 56 | self.publication = item[u"publicationTitle"] 57 | else: 58 | self.publication = None 59 | if u"title" in item: 60 | self.title = item[u"title"] 61 | else: 62 | self.title = None 63 | if u"author" in item: 64 | self.authors = item[u"author"] 65 | else: 66 | self.authors = [] 67 | if u"date" in item: 68 | self.date = item[u"date"] 69 | else: 70 | self.date = None 71 | if u"issue" in item: 72 | self.issue = item[u"issue"] 73 | else: 74 | self.issue = None 75 | if u"volume" in item: 76 | self.volume = item[u"volume"] 77 | else: 78 | self.volume = None 79 | if u"fulltext" in item: 80 | self.fulltext = item[u"fulltext"] 81 | else: 82 | self.fulltext = None 83 | if u"collections" in item: 84 | self.collections = item[u"collections"] 85 | else: 86 | self.collections = [] 87 | if u"tags" in item: 88 | self.tags = item[u"tags"] 89 | else: 90 | self.tags = [] 91 | if u"key" in item: 92 | self.key = item[u"key"] 93 | else: 94 | self.key = None 95 | else: 96 | self.title = None 97 | self.collections = [] 98 | self.publication = None 99 | self.authors = [] 100 | self.tags = [] 101 | self.issue = None 102 | self.volume = None 103 | self.fulltext = None 104 | self.date = None 105 | self.key = None 106 | if isinstance(init, int): 107 | self.id = init 108 | else: 109 | self.id = None 110 | 111 | def match(self, terms): 112 | 113 | """ 114 | Matches the current item against a term. 115 | 116 | Arguments: 117 | terms -- A list of (term_type, term) tuples. 118 | 119 | Returns: 120 | True if the current item matches the terms, False otherwise. 121 | """ 122 | 123 | global term_collection, term_author, term_title, term_date, \ 124 | term_publication, term_tag 125 | 126 | # Author is a required field. Without it we don't search 127 | if len(self.authors) > 0: 128 | # Do all criteria match? 129 | match_all = True 130 | # Walk through all search terms 131 | for term_type, term in terms: 132 | match = False 133 | if term_type in term_tag: 134 | for tag in self.tags: 135 | if term in tag.lower(): 136 | match = True 137 | if term_type in term_collection: 138 | for collection in self.collections: 139 | if term in collection.lower(): 140 | match = True 141 | if not match and term_type in term_author: 142 | for author in self.authors: 143 | if term in author.lower(): 144 | match = True 145 | if not match and self.date != None and term_type in term_date: 146 | if term in self.date: 147 | match = True 148 | if not match and self.title != None and term_type in \ 149 | term_title and term in self.title.lower(): 150 | match = True 151 | if not match and self.publication != None and term_type in \ 152 | term_publication and term in self.publication.lower(): 153 | match = True 154 | if not match: 155 | match_all = False 156 | break 157 | return match_all 158 | return False 159 | 160 | def get_note(self): 161 | 162 | """ 163 | Retrieves a note. 164 | 165 | Returns: 166 | A note for the current item. 167 | """ 168 | 169 | if self.note != -1: 170 | return self.note 171 | self.note = self.noteProvider.search(self) 172 | return self.note 173 | 174 | def format_author(self): 175 | 176 | """ 177 | Returns: 178 | A pretty representation of the author. 179 | """ 180 | 181 | if self.authors == []: 182 | return u"Unkown author" 183 | if len(self.authors) > 5: 184 | return u"%s et al." % self.authors[0] 185 | if len(self.authors) > 2: 186 | return u", ".join(self.authors[:-1]) + u", & " + self.authors[-1] 187 | if len(self.authors) == 2: 188 | return self.authors[0] + u" & " + self.authors[1] 189 | return self.authors[0] 190 | 191 | def format_date(self): 192 | 193 | """ 194 | Returns: 195 | A pretty representation of the date. 196 | """ 197 | 198 | if self.date == None: 199 | return u"(Date unknown)" 200 | 201 | return u"(%s)" % self.date 202 | 203 | def format_title(self): 204 | 205 | """ 206 | Returns: 207 | A pretty representation of the title. 208 | """ 209 | 210 | if self.title == None: 211 | return u"Unknown title" 212 | return self.title 213 | 214 | def format_publication(self): 215 | 216 | """ 217 | Returns: 218 | A pretty representation of the publication (journal). 219 | """ 220 | 221 | if self.publication == None: 222 | return u"Unknown journal" 223 | return self.publication 224 | 225 | def format_tags(self): 226 | 227 | """ 228 | Returns: 229 | A pretty representation of the tags. 230 | """ 231 | 232 | return u", ".join(self.tags) 233 | 234 | def gnotero_format(self): 235 | 236 | """ 237 | Returns: 238 | A pretty apa-like representation of the item, which can be used as a 239 | label in Qnotero. 240 | """ 241 | 242 | if self.gnotero_format_str == None: 243 | s = u"" + self.format_author() + u" " + self.format_date() + \ 244 | u"" 245 | if self.title != None: 246 | s += u"\n" + self.title 247 | if self.publication != None: 248 | s += u"\n" + self.publication 249 | if self.volume != None: 250 | s += u", %s" % self.volume 251 | s += u"" 252 | if self.issue != None: 253 | s += u"(%s)" % self.issue 254 | s += u"" 255 | self.gnotero_format_str = s.replace(u"&", u"&") 256 | return self.gnotero_format_str 257 | 258 | def full_format(self): 259 | 260 | """ 261 | Returns: 262 | A pretty, extensive representation of the current item. 263 | """ 264 | 265 | if self.gnotero_format_str == None: 266 | s = self.format_author() + u" " + self.format_date() 267 | if self.title != None: 268 | s += u"\n" + self.title 269 | if self.publication != None: 270 | s += u"\n" + self.publication 271 | if self.volume != None: 272 | s += u", %s" % self.volume 273 | if self.issue != None: 274 | s += u"(%s)" % self.issue 275 | else: 276 | s += u"\n" 277 | if self.tags != None: 278 | s += u"\n" + self.format_tags() 279 | self.gnotero_format_str = s 280 | return self.gnotero_format_str 281 | 282 | def simple_format(self): 283 | 284 | """ 285 | Returns: 286 | A pretty, simple representation of the current item. 287 | """ 288 | 289 | if self.simple_format_str == None: 290 | self.simple_format_str = self.format_author() + u" " + \ 291 | self.format_date() 292 | return self.simple_format_str 293 | 294 | def filename_format(self): 295 | 296 | """ 297 | Returns: 298 | A pretty filename format representation of the current item. 299 | """ 300 | 301 | if self.filename_format_str == None: 302 | self.filename_format_str = self.format_author() + u" " + \ 303 | self.format_date().replace(u"\\", u"") 304 | return self.filename_format_str 305 | 306 | def hashKey(self): 307 | 308 | """ 309 | Returns: 310 | A hash representation of the current object. 311 | """ 312 | 313 | global cache 314 | hashKey = str(self) 315 | cache[hashKey] = self 316 | return hashKey 317 | -------------------------------------------------------------------------------- /qnotero: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | This file is part of qnotero. 5 | 6 | qnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | qnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with qnotero. If not, see . 18 | """ 19 | 20 | if __name__ == "__main__": 21 | import sys 22 | # Check if we're running a supported Python (>= 3.3) 23 | if sys.version_info < (3,3,0): 24 | raise Exception('Qnotero requires Python >= 3.3') 25 | if '--version' in sys.argv: 26 | from libqnotero.qnotero import Qnotero 27 | print(Qnotero.version) 28 | sys.exit() 29 | print('Using Python %s' % sys.version) 30 | # The listener will fail if another instance of Qnotero is already running. 31 | # In that case we send an activate signal, to pop up the Qnotero window, and 32 | # exit. 33 | from libqnotero.listener import Listener 34 | try: 35 | if "--notray" not in sys.argv: 36 | listener = Listener() 37 | else: 38 | listener = None 39 | except: 40 | from libqnotero.config import getConfig 41 | import socket 42 | print(u"qnotero: Qnotero already running, sending activate signal") 43 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 44 | sock.sendto(b"activate", (u"localhost", getConfig(u"listenerPort")) ) 45 | sys.exit() 46 | 47 | # Enable CTRL+C. 48 | import signal 49 | signal.signal(signal.SIGINT, signal.SIG_DFL) 50 | 51 | # Normal start of Qnotero 52 | from libqnotero.qnotero import Qnotero 53 | from libqnotero.qt.QtGui import QApplication 54 | reset = "--reset" in sys.argv 55 | systray = "--notray" not in sys.argv 56 | app = QApplication(sys.argv) 57 | app.setQuitOnLastWindowClosed(False) 58 | qnotero = Qnotero(systray=systray, debug=True, reset=reset) 59 | qnotero.listener = listener 60 | if not systray: 61 | qnotero.popUp() 62 | if listener != None: 63 | listener.qnotero = qnotero 64 | listener.start() 65 | sys.exit(app.exec_()) 66 | -------------------------------------------------------------------------------- /qnotero.nsi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/qnotero.nsi -------------------------------------------------------------------------------- /readme-src.md: -------------------------------------------------------------------------------- 1 | Qnotero v%-- exec: ./qnotero --version --% 2 | 3 | *Copyright 2011-2015 Sebastiaan Mathôt* 4 | 5 | ## Overview 6 | 7 | %-- 8 | toc: 9 | mindepth: 2 10 | exclude: [Overview] 11 | --% 12 | 13 | ## What is Qnotero? 14 | 15 | Qnotero provides lightning quick access to your Zotero references. Zotero is an excellent open source reference manager, but it lacks a simple and direct way to access your references at the click of a button. That is why I created this simple program, which lives in the system tray and allows you to search through your references by Author and/ or Year of Publication. If a PDF file is attached to a reference you can open it directly from within Qnotero. 16 | 17 | Freely available under the [GNU GPL 3](http://www.gnu.org/copyleft/gpl.html). 18 | 19 | ## Download and installation 20 | 21 | ### Windows 22 | 23 | Windows binaries can be downloaded from GitHub: 24 | 25 | - 26 | 27 | ### Debian/ Ubuntu/ Linux Mint 28 | 29 | Ubuntu/ Linux Mint users can install Qnotero through the [Cogsci.nl PPA]: 30 | 31 | sudo add-apt-repository ppa:smathot/cogscinl 32 | sudo apt-get update 33 | sudo apt-get install qnotero 34 | 35 | ### Mac OS 36 | 37 | There is no Qnotero package available for Mac OS. It should be possible to run Qnotero from source, if you have all the dependencies installed (notably Python 3 and PyQt5). Please let me know of any experiences running Qnotero on Mac OS (good or bad). 38 | 39 | ### Other operating systems 40 | 41 | For other operating systems, you can (try to) run Qnotero from source. Source code for stable releases can be downloaded from GitHub: 42 | 43 | - 44 | 45 | ## Dependencies 46 | 47 | Qnotero has the following dependencies. 48 | 49 | - [Python] -- As of Qnotero 1.0.0, Python >= 3.3 is required. 50 | - [PyQt4] -- Pass `--qt5` as command-line argument for experimental PyQt5 support. 51 | 52 | ## Gnote integration (Linux only) 53 | 54 | If you have Gnote installed (a note-taking program for Linux), Qnotero automatically searches Gnote for a (section in a) note belonging to a specific article. Qnotero expects each note to be preceded by a bold line containing at least the name of the first author and the year of publication within parentheses, like so: 55 | 56 | Duhamel et al. (1992) Science 255 57 | 58 | ## Install Zotero standalone on Linux 59 | 60 | ### Automated Zotero standalone installer script 61 | 62 | Linux users need to install Zotero standalone manually from a .tar.gz archive. This is a little inconvenient, which is why I created a simple installation script. This script downloads the latest version of Zotero standalone and creates a menu entry. If you use Ubuntu or Linux Mint, you can also use the PPA (see below). 63 | 64 | To run the automated installer, type (or copy-paste) the following in a terminal: 65 | 66 | wget https://raw.github.com/smathot/zotero_installer/master/zotero_installer.sh \ 67 | -O /tmp/zotero_installer.sh 68 | chmod +x /tmp/zotero_installer.sh 69 | /tmp/zotero_installer.sh 70 | 71 | ### Zotero standalone PPA for Ubuntu/ Linux Mint 72 | 73 | Ubuntu/ Linux Mint users can install Zotero standalone from the [Cogsci.nl PPA]. Note that this method of installation is essentially a wrapper around the installation script (see above) and will therefore provide some unusual terminal output. 74 | 75 | sudo add-apt-repository ppa:smathot/cogscinl 76 | sudo apt-get update 77 | sudo apt-get install zotero-standalone 78 | 79 | If you have previously installed Zotero standalone using the installer script above, you will need to remove the installed files before re-installing from the PPA. For a local installation, these are: 80 | 81 | /home/[user]/zotero 82 | /home/[user]/.local/share/applications/zotero.desktop 83 | 84 | For a global installation, these are: 85 | 86 | /opt/zotero 87 | /usr/local/applications/zotero.desktop 88 | 89 | ## Support and feedback 90 | 91 | There are a number of channels through which you can ask questions and provide feedback: 92 | 93 | - The comment section below is a good place for brief comments/ simple questions. 94 | - For (potentially) lengthy discussions, please use the [forum]. 95 | 96 | [cogsci.nl ppa]: https://launchpad.net/~smathot/+archive/ubuntu/cogscinl 97 | [forum]: http://forum.cogsci.nl/ 98 | [python]: https://www.python.org/ 99 | [PyQt4]: http://www.riverbankcomputing.co.uk/software/pyqt/download 100 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | *Update: This repository is no longer maintained. For a fork that is actively developed see * 2 | 3 | Qnotero v2.0.0 4 | 5 | 6 | *Copyright 2011-2015 Sebastiaan Mathôt* 7 | 8 | ## Overview 9 | 10 | 11 | - [What is Qnotero?](#what-is-qnotero) 12 | - [Download and installation](#download-and-installation) 13 | - [Windows](#windows) 14 | - [Debian/ Ubuntu/ Linux Mint](#debian-ubuntu-linux-mint) 15 | - [Mac OS](#mac-os) 16 | - [Other operating systems](#other-operating-systems) 17 | - [Dependencies](#dependencies) 18 | - [Gnote integration (Linux only)](#gnote-integration-linux-only) 19 | - [Install Zotero standalone on Linux](#install-zotero-standalone-on-linux) 20 | - [Automated Zotero standalone installer script](#automated-zotero-standalone-installer-script) 21 | - [Zotero standalone PPA for Ubuntu/ Linux Mint](#zotero-standalone-ppa-for-ubuntu-linux-mint) 22 | - [Support and feedback](#support-and-feedback) 23 | 24 | 25 | 26 | ## What is Qnotero? 27 | 28 | Qnotero provides lightning quick access to your Zotero references. Zotero is an excellent open source reference manager, but it lacks a simple and direct way to access your references at the click of a button. That is why I created this simple program, which lives in the system tray and allows you to search through your references by Author and/ or Year of Publication. If a PDF file is attached to a reference you can open it directly from within Qnotero. 29 | 30 | Freely available under the [GNU GPL 3](http://www.gnu.org/copyleft/gpl.html). 31 | 32 | ## Download and installation 33 | 34 | ### Windows 35 | 36 | Windows binaries can be downloaded from GitHub: 37 | 38 | - 39 | 40 | ### Debian/ Ubuntu/ Linux Mint 41 | 42 | Ubuntu/ Linux Mint users can install Qnotero through the [Cogsci.nl PPA]: 43 | 44 | sudo add-apt-repository ppa:smathot/cogscinl 45 | sudo apt-get update 46 | sudo apt-get install qnotero 47 | 48 | ### Mac OS 49 | 50 | There is no Qnotero package available for Mac OS. It should be possible to run Qnotero from source, if you have all the dependencies installed (notably Python 3 and PyQt5). Please let me know of any experiences running Qnotero on Mac OS (good or bad). 51 | 52 | ### Other operating systems 53 | 54 | For other operating systems, you can (try to) run Qnotero from source. Source code for stable releases can be downloaded from GitHub: 55 | 56 | - 57 | 58 | ## Dependencies 59 | 60 | Qnotero has the following dependencies. 61 | 62 | - [Python] -- As of Qnotero 1.0.0, Python >= 3.3 is required. 63 | - [PyQt4] -- Pass `--qt5` as command-line argument for experimental PyQt5 support. 64 | 65 | ## Gnote integration (Linux only) 66 | 67 | If you have Gnote installed (a note-taking program for Linux), Qnotero automatically searches Gnote for a (section in a) note belonging to a specific article. Qnotero expects each note to be preceded by a bold line containing at least the name of the first author and the year of publication within parentheses, like so: 68 | 69 | Duhamel et al. (1992) Science 255 70 | 71 | ## Install Zotero standalone on Linux 72 | 73 | ### Automated Zotero standalone installer script 74 | 75 | Linux users need to install Zotero standalone manually from a .tar.gz archive. This is a little inconvenient, which is why I created a simple installation script. This script downloads the latest version of Zotero standalone and creates a menu entry. If you use Ubuntu or Linux Mint, you can also use the PPA (see below). 76 | 77 | To run the automated installer, type (or copy-paste) the following in a terminal: 78 | 79 | wget https://raw.github.com/smathot/zotero_installer/master/zotero_installer.sh \ 80 | -O /tmp/zotero_installer.sh 81 | chmod +x /tmp/zotero_installer.sh 82 | /tmp/zotero_installer.sh 83 | 84 | ### Zotero standalone PPA for Ubuntu/ Linux Mint 85 | 86 | Ubuntu/ Linux Mint users can install Zotero standalone from the [Cogsci.nl PPA]. Note that this method of installation is essentially a wrapper around the installation script (see above) and will therefore provide some unusual terminal output. 87 | 88 | sudo add-apt-repository ppa:smathot/cogscinl 89 | sudo apt-get update 90 | sudo apt-get install zotero-standalone 91 | 92 | If you have previously installed Zotero standalone using the installer script above, you will need to remove the installed files before re-installing from the PPA. For a local installation, these are: 93 | 94 | /home/[user]/zotero 95 | /home/[user]/.local/share/applications/zotero.desktop 96 | 97 | For a global installation, these are: 98 | 99 | /opt/zotero 100 | /usr/local/applications/zotero.desktop 101 | 102 | ## Support and feedback 103 | 104 | There are a number of channels through which you can ask questions and provide feedback: 105 | 106 | - The comment section below is a good place for brief comments/ simple questions. 107 | - For (potentially) lengthy discussions, please use the [forum]. 108 | 109 | [cogsci.nl ppa]: https://launchpad.net/~smathot/+archive/ubuntu/cogscinl 110 | [forum]: http://forum.cogsci.nl/ 111 | [python]: https://www.python.org/ 112 | [PyQt4]: http://www.riverbankcomputing.co.uk/software/pyqt/download 113 | 114 | [Overview]: #overview 115 | [What is Qnotero?]: #what-is-qnotero 116 | [Download and installation]: #download-and-installation 117 | [Windows]: #windows 118 | [Debian/ Ubuntu/ Linux Mint]: #debian-ubuntu-linux-mint 119 | [Mac OS]: #mac-os 120 | [Other operating systems]: #other-operating-systems 121 | [Dependencies]: #dependencies 122 | [Gnote integration (Linux only)]: #gnote-integration-linux-only 123 | [Install Zotero standalone on Linux]: #install-zotero-standalone-on-linux 124 | [Automated Zotero standalone installer script]: #automated-zotero-standalone-installer-script 125 | [Zotero standalone PPA for Ubuntu/ Linux Mint]: #zotero-standalone-ppa-for-ubuntu-linux-mint 126 | [Support and feedback]: #support-and-feedback 127 | -------------------------------------------------------------------------------- /readme.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | This file is part of qnotero. 5 | 6 | qnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | qnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with qnotero. If not, see . 18 | """ 19 | 20 | from academicmarkdown import build 21 | build.MD('readme-src.md', 'readme.md') 22 | build.HTML('readme-src.md', 'readme.html') 23 | -------------------------------------------------------------------------------- /resources/default/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/default/close.png -------------------------------------------------------------------------------- /resources/default/nopdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/default/nopdf.png -------------------------------------------------------------------------------- /resources/default/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/default/pdf.png -------------------------------------------------------------------------------- /resources/default/preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/default/preferences.png -------------------------------------------------------------------------------- /resources/default/qnotero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/default/qnotero.png -------------------------------------------------------------------------------- /resources/default/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/default/search.png -------------------------------------------------------------------------------- /resources/default/stylesheet.qss: -------------------------------------------------------------------------------- 1 | QLabel, 2 | QCheckBox, 3 | #Qnotero, 4 | #Preferences, 5 | #labelInfoMsg, 6 | #labelInfoMsgHeader, 7 | #labelResultMsg { 8 | color: #eeeeec; 9 | background: #555753; 10 | } 11 | 12 | #Preferences QLineEdit, 13 | #Preferences QSpinBox, 14 | #Preferences QComboBox, 15 | #Preferences QCheckBox, 16 | QPushButton { 17 | border-radius: 4px; 18 | padding: 4px; 19 | } 20 | 21 | #widgetSearch, 22 | QPushButton { 23 | color: #eeeeec; 24 | background: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #888a85, stop: 1 #2e3436); 25 | } 26 | 27 | QSpinBox, 28 | QLineEdit, 29 | QComboBox, 30 | #listWidgetResults { 31 | background: #888a85; 32 | color: #eeeeec; 33 | } 34 | 35 | #lineEditQuery { 36 | background: transparent; 37 | color: #eeeeec; 38 | } 39 | 40 | #widgetSearch, 41 | #listWidgetResults { 42 | border-radius: 8px; 43 | } 44 | 45 | #lineEditQuery { 46 | font-size: 14pt; 47 | margin-left: 8px; 48 | } 49 | 50 | #labelInfoMsgHeader { 51 | font-weight: bold; 52 | } 53 | 54 | #labelLocatePath, 55 | #labelNoteAvailable { 56 | font-style: italic; 57 | } 58 | 59 | QLineEdit, 60 | QComboBox, 61 | #listWidgetResults { 62 | selection-background-color: #555753; 63 | } 64 | 65 | #labelFirstRun { 66 | border-radius: 4px; 67 | padding: 8px; 68 | color: #eeeeec; 69 | margin-bottom: 16px; 70 | margin-top: 16px; 71 | background: #888a85; 72 | } -------------------------------------------------------------------------------- /resources/elementary/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 16 | 20 | 24 | 28 | 29 | 31 | 35 | 39 | 40 | 42 | 46 | 50 | 51 | 53 | 57 | 61 | 65 | 69 | 70 | 72 | 76 | 80 | 81 | 90 | 100 | 109 | 119 | 129 | 137 | 138 | 142 | 146 | 153 | 161 | 168 | 169 | 170 | 179 | 188 | 192 | 196 | 200 | 201 | 205 | 209 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /resources/elementary/nopdf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 14 | 16 | 20 | 24 | 28 | 29 | 31 | 35 | 39 | 40 | 42 | 46 | 50 | 51 | 53 | 57 | 61 | 62 | 68 | 72 | 76 | 80 | 84 | 88 | 92 | 96 | 97 | 99 | 103 | 107 | 108 | 114 | 118 | 119 | 121 | 125 | 129 | 130 | 139 | 148 | 156 | 165 | 174 | 184 | 194 | 203 | 204 | 206 | 213 | 217 | 221 | 225 | 229 | 233 | 238 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /resources/elementary/pdf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 31 | 33 | 37 | 41 | 42 | 52 | 54 | 58 | 62 | 63 | 74 | 76 | 80 | 84 | 88 | 92 | 93 | 103 | 105 | 109 | 113 | 114 | 125 | 127 | 131 | 135 | 136 | 147 | 149 | 153 | 157 | 158 | 167 | 169 | 173 | 177 | 181 | 182 | 184 | 188 | 192 | 196 | 200 | 201 | 211 | 212 | 231 | 233 | 234 | 236 | image/svg+xml 237 | 239 | 240 | 241 | 242 | 243 | 247 | 251 | 255 | 262 | 270 | 277 | 278 | 279 | 288 | 292 | 296 | 300 | 304 | 308 | 317 | 318 | 319 | -------------------------------------------------------------------------------- /resources/elementary/preferences.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 30 | 33 | 37 | 41 | 42 | 51 | 61 | 63 | 67 | 71 | 72 | 82 | 89 | 93 | 97 | 98 | 108 | 110 | 114 | 118 | 119 | 121 | 125 | 129 | 130 | 141 | 142 | 161 | 163 | 164 | 166 | image/svg+xml 167 | 169 | 170 | 171 | 172 | 173 | 177 | 181 | 185 | 189 | 193 | 198 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /resources/elementary/qnotero.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 50 | 52 | 61 | 63 | 67 | 71 | 72 | 82 | 84 | 88 | 92 | 93 | 103 | 105 | 109 | 113 | 117 | 118 | 127 | 129 | 133 | 137 | 138 | 147 | 149 | 153 | 157 | 158 | 167 | 169 | 173 | 177 | 181 | 182 | 192 | 194 | 198 | 202 | 203 | 213 | 224 | 226 | 230 | 234 | 235 | 245 | 247 | 251 | 255 | 256 | 267 | 269 | 273 | 277 | 281 | 285 | 286 | 296 | 298 | 302 | 306 | 307 | 317 | 326 | 328 | 332 | 336 | 340 | 341 | 351 | 353 | 357 | 361 | 362 | 373 | 383 | 394 | 404 | 415 | 417 | 421 | 425 | 429 | 433 | 434 | 444 | 446 | 450 | 454 | 455 | 456 | 459 | 466 | 470 | 474 | 475 | 479 | 483 | 487 | 491 | Aa 502 | Aa 513 | 517 | 518 | -------------------------------------------------------------------------------- /resources/elementary/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 16 | 20 | 24 | 25 | 35 | 37 | 41 | 45 | 46 | 55 | 57 | 61 | 65 | 66 | 75 | 77 | 81 | 85 | 86 | 95 | 97 | 101 | 105 | 106 | 115 | 117 | 121 | 125 | 126 | 136 | 138 | 142 | 146 | 147 | 156 | 158 | 162 | 166 | 167 | 176 | 178 | 182 | 186 | 187 | 196 | 197 | 199 | 203 | 207 | 212 | 217 | 221 | 225 | 229 | 230 | 231 | -------------------------------------------------------------------------------- /resources/elementary/stylesheet.qss: -------------------------------------------------------------------------------- 1 | QLabel, 2 | QCheckBox, 3 | QLineEdit, 4 | #Qnotero, 5 | #Preferences, 6 | #labelInfoMsg, 7 | #labelInfoMsgHeader, 8 | #labelResultMsg { 9 | color: #555753; 10 | background: #eeeeec; 11 | } 12 | 13 | QLineEdit, 14 | QComboBox, 15 | QPushButton { 16 | border-radius: 4px; 17 | border: 2px solid #729fcf; 18 | margin: 4px; 19 | } 20 | 21 | #listWidgetResults, 22 | #widgetSearch { 23 | color: #555753; 24 | background: #eeeeec; 25 | border-radius: 4px; 26 | padding: 8px; 27 | border: 2px solid #729fcf; 28 | selection-background-color: #729fcf; 29 | } 30 | 31 | #widgetSearch QPushButton, 32 | #widgetSearch QLineEdit { 33 | border: none; 34 | } 35 | 36 | #lineEditQuery { 37 | font-size: 14pt; 38 | margin-left: 8px; 39 | } -------------------------------------------------------------------------------- /resources/tango/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/tango/close.png -------------------------------------------------------------------------------- /resources/tango/nopdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/tango/nopdf.png -------------------------------------------------------------------------------- /resources/tango/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/tango/pdf.png -------------------------------------------------------------------------------- /resources/tango/preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/tango/preferences.png -------------------------------------------------------------------------------- /resources/tango/qnotero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/tango/qnotero.png -------------------------------------------------------------------------------- /resources/tango/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smathot/qnotero/1f91f6b4ca14655dcdfe9b7674d69e39fd4b8fa3/resources/tango/search.png -------------------------------------------------------------------------------- /resources/tango/stylesheet.qss: -------------------------------------------------------------------------------- 1 | QLabel, 2 | QCheckBox, 3 | #Qnotero, 4 | #Preferences, 5 | #labelInfoMsg, 6 | #labelInfoMsgHeader, 7 | #labelResultMsg { 8 | color: #eeeeec; 9 | background: #3465a4; 10 | } 11 | 12 | #Preferences QLineEdit, 13 | #Preferences QSpinBox, 14 | #Preferences QComboBox, 15 | #Preferences QCheckBox, 16 | QPushButton { 17 | border-radius: 4px; 18 | padding: 4px; 19 | } 20 | 21 | #widgetSearch, 22 | QPushButton { 23 | color: #eeeeec; 24 | background: QLinearGradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #4e9a06, stop: 1 #8ae234); 25 | } 26 | 27 | QSpinBox, 28 | QLineEdit, 29 | QComboBox, 30 | #listWidgetResults { 31 | background: #729fcf; 32 | color: #eeeeec; 33 | } 34 | 35 | #lineEditQuery { 36 | background: transparent; 37 | color: #eeeeec; 38 | } 39 | 40 | #widgetSearch, 41 | #listWidgetResults { 42 | border-radius: 8px; 43 | } 44 | 45 | #lineEditQuery { 46 | font-size: 14pt; 47 | margin-left: 8px; 48 | } 49 | 50 | #labelInfoMsgHeader { 51 | font-weight: bold; 52 | } 53 | 54 | #labelLocatePath, 55 | #labelNoteAvailable { 56 | font-style: italic; 57 | } 58 | 59 | QLineEdit, 60 | QComboBox, 61 | #listWidgetResults { 62 | selection-background-color: #f57900; 63 | } 64 | 65 | #labelFirstRun { 66 | border-radius: 4px; 67 | padding: 8px; 68 | color: #eeeeec; 69 | margin-bottom: 16px; 70 | margin-top: 16px; 71 | background: #888a85; 72 | } 73 | -------------------------------------------------------------------------------- /setup-windows.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | This file is part of qnotero. 5 | 6 | qnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | qnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with qnotero. If not, see . 18 | 19 | --- 20 | desc: 21 | Windows packaging procedure: 22 | 23 | 1. Build Qnotero into `dist` with `setup-win.py py2exe` 24 | 2. Create `.exe` installer with `.nsi` script 25 | 3. Rename `.exe` installer 26 | 4. Rename `dist` and pack it into `.zip` for portable distribution 27 | --- 28 | """ 29 | 30 | from distutils.core import setup 31 | import glob 32 | import py2exe 33 | import os 34 | import shutil 35 | import sys 36 | 37 | class safe_print(object): 38 | 39 | """ 40 | desc: 41 | Used to redirect standard output, so that Python doesn't crash when 42 | printing special characters to a terminal. 43 | """ 44 | 45 | errors = 'strict' 46 | encoding = 'utf-8' 47 | 48 | def write(self, msg): 49 | if isinstance(msg, str): 50 | msg = msg.encode('ascii', 'ignore') 51 | sys.__stdout__.write(msg.decode('ascii')) 52 | 53 | def flush(self): 54 | pass 55 | 56 | # Redirect standard output to safe printer 57 | sys.stdout = safe_print() 58 | 59 | # Create empty destination folders 60 | if os.path.exists("dist"): 61 | shutil.rmtree("dist") 62 | os.mkdir("dist") 63 | 64 | # Setup options 65 | setup( 66 | windows = [{ 67 | "script" : "qnotero", 68 | 'icon_resources': [ 69 | (0, os.path.join("data", "qnotero.ico")) 70 | ], 71 | }], 72 | data_files = [ 73 | ('resources/default', glob.glob('resources/default/*')), 74 | ('resources/elementary', glob.glob('resources/elementary/*')), 75 | ('resources/tango', glob.glob('resources/tango/*')), 76 | ('libqnotero/ui', glob.glob('libqnotero/ui/*')), 77 | ('data', ['data/qnotero.ico']) 78 | ], 79 | options = { 80 | 'py2exe' : { 81 | 'compressed' : True, 82 | 'optimize': 2, 83 | 'bundle_files': 3, 84 | 'includes': [ 85 | 'sip', 86 | ], 87 | 'packages' : [ 88 | "libqnotero", 89 | "libzotero", 90 | "libqnotero._themes", 91 | "libzotero._noteProvider", 92 | ], 93 | "dll_excludes" : ["MSVCP90.DLL"], 94 | }, 95 | }, 96 | ) 97 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | This file is part of qnotero. 5 | 6 | qnotero is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | qnotero is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with qnotero. If not, see . 18 | """ 19 | 20 | import glob 21 | from libqnotero.qnotero import Qnotero 22 | from distutils.core import setup 23 | 24 | setup(name="qnotero", 25 | version = Qnotero.version, 26 | description = "Standalone sidekick to the Zotero reference manager", 27 | author = "Sebastiaan Mathot", 28 | author_email = "s.mathot@cogsci.nl", 29 | url = "http://www.cogsci.nl/", 30 | scripts = ["qnotero"], 31 | packages = [ 32 | "libqnotero", 33 | "libzotero", 34 | "libqnotero._themes", 35 | "libzotero._noteProvider", 36 | "libqnotero.qt" 37 | ], 38 | package_data = { 39 | "libqnotero" : ["ui/*.ui"], 40 | }, 41 | data_files=[ 42 | ("/usr/share/qnotero", ["COPYING"]), 43 | ("/usr/share/applications", ["data/qnotero.desktop"]), 44 | ("/usr/share/qnotero/resources/default", 45 | glob.glob("resources/default/*")), 46 | ("/usr/share/qnotero/resources/elementary", 47 | glob.glob("resources/elementary/*")), 48 | ("/usr/share/qnotero/resources/tango", 49 | glob.glob("resources/tango/*")), 50 | ] 51 | ) 52 | --------------------------------------------------------------------------------