From cac325232888e6270a228288c95a12c2236c54b0 Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Mon, 2 Nov 2020 12:36:35 -0600 Subject: [PATCH 01/12] Replace doc_base with resources This copies the FAQ and LICENSE into the package so they are available as a resource. --- src/gourmet/GourmetRecipeManager.py | 15 +- src/gourmet/data/FAQ | 300 +++++++++++++++++++++ src/gourmet/data/LICENSE | 339 ++++++++++++++++++++++++ src/gourmet/gglobals.py | 1 - src/gourmet/gtk_extras/dialog_extras.py | 52 ++-- src/gourmet/reccard.py | 7 +- src/gourmet/settings.py | 1 - src/gourmet/shopgui.py | 4 +- 8 files changed, 677 insertions(+), 42 deletions(-) create mode 100644 src/gourmet/data/FAQ create mode 100644 src/gourmet/data/LICENSE diff --git a/src/gourmet/GourmetRecipeManager.py b/src/gourmet/GourmetRecipeManager.py index eae4af6b3..8560990b5 100644 --- a/src/gourmet/GourmetRecipeManager.py +++ b/src/gourmet/GourmetRecipeManager.py @@ -1,5 +1,6 @@ import os import os.path +from pkgutil import get_data as _get_data import re import threading from gettext import gettext as _ @@ -15,7 +16,7 @@ from gourmet.exporters.exportManager import ExportManager from gourmet.exporters.printer import PrintManager from gourmet.gdebug import debug -from gourmet.gglobals import (DEFAULT_HIDDEN_COLUMNS, REC_ATTRS, doc_base, +from gourmet.gglobals import (DEFAULT_HIDDEN_COLUMNS, REC_ATTRS, icondir, uibase) from gourmet.gtk_extras import WidgetSaver from gourmet.gtk_extras import dialog_extras as de @@ -350,12 +351,8 @@ def show_about (self, *args): logo=GdkPixbuf.Pixbuf.new_from_file(os.path.join(icondir,"gourmet.png")) # load LICENSE text file - try: - license_text = open(os.path.join(doc_base,'LICENSE'),'r').read() - except IOError as err: - print("IO Error %s" % err) - except: - print("Unexpexted error") + license_text = _get_data('gourmet', 'data/LICENSE').decode() + assert license_text paypal_link = """https://www.paypal.com/cgi-bin/webscr?cmd=_donations &business=Thomas_Hinkle%40alumni%2ebrown%2eedu @@ -364,7 +361,7 @@ def show_about (self, *args): gratipay_link = "https://gratipay.com/on/github/thinkle/" flattr_link = "http://flattr.com/profile/Thomas_Hinkle/things" - about = Gtk.AboutDialog() + about = Gtk.AboutDialog(parent=self.window) about.set_artists(version.artists) about.set_authors(version.authors) about.set_comments(version.description) @@ -404,7 +401,7 @@ def show_about (self, *args): about.destroy() def show_help (self, *args): - de.show_faq(os.path.join(doc_base,'FAQ')) + de.show_faq(parent=self.window) def save (self, file=None, db=None, xml=None): debug("save (self, file=None, db=None, xml=None):",5) diff --git a/src/gourmet/data/FAQ b/src/gourmet/data/FAQ new file mode 100644 index 000000000..3027b4e31 --- /dev/null +++ b/src/gourmet/data/FAQ @@ -0,0 +1,300 @@ +1. What is this F.A.Q. for? + +Gourmet is intended to be as intuitive to use as possible. If +you find something in gourmet counterintuitive, confusing, or +hard-to-use, I'll consider it a bug and try my best to fix it. + +Although Gourmet is intended to be intuitive to use, it is not always +so. Rather than write a complete manual, I've put together a brief +list of pointers in the form of this F.A.Q., trying to focus on those +elements of Gourmet that are not as transparent to the user. + +2. Getting recipes + +2.a Where can I get some recipes for Gourmet? + +Gourmet can easily import mealmaster and mastercook files. There are +large archives of these files available on the web -- search for the +kind of recipes you're interested in and "mealmaster" or "mastercook", +and you should find them. + +Gourmet can also import recipes from websites. A very few websites +have support for automated import. In most cases, Gourmet will pop up +a window asking you to identify the title, source, ingredients, +etc. from the text of the recipe on the webpage. Then Gourmet will ask +if you want to choose any images from the webpage to associate with +the web page. + +2.b. What is that funny dialog about "encoding" that comes up when I import? + +There exist many different ways to "encode" the same characters. When +a recipe includes accented characters or symbols (like the degree +symbol), it is important that Gourmet choose the right encoding to +read the recipe, or all of these characters will come out wrong. + +When Gourmet encounters a file with unusual characters, it limits the +possible encodings to as few choices as possible and then presents you +with a choice, showing you the file with different encodings. The +lines where the different encodings result in different characters are +highlighted yellow, so you can quickly scroll to them and see which +encoding looks right. + +3. How do I do complicated searches? + +By default, Gourmet does a simple search for the text you enter +anywhere. If you want to combine multiple searches, just hit return +and start typing again. For example, to search for desserts with +oranges, type "dessert", hit return, and then type "orange." + +You can also type "or" to search for one thing or another. + +By clicking "Show Search Options," you can further narrow your search +results, searching only in a given part of a recipe (e.g. in +ingredients). You can also turn on support for regular expressions, +which will allow programmers and others familiar with regular +expressions to do more powerful searches. + +3.b Searching is too slow, what can I do? + +If your computer is slow and/or your database is big, you may find that +Gourmet is sluggish when you are searching. This is because, by default, +Gourmet tries to search as you type. By clicking "Show Search Options" +and then unchecking "Search as you type," you can turn off this feature. +In this mode, you have to type "return" or click "find" to actually run +a search. + +If Gourmet is still sluggish in this case, you may need to select more +specific searches instead of using the default "search anywhere" option. +For most users this should be no problem, but it is possible that with +a very large database and a slow machine, you will run into performance +problems. + +4. Shopping lists + +4.a. How do I create a shopping list? + +There are two ways: Click on the "Shop" button in a recipe card, or, +select "Add to shopping list" from the right-click menu or the Action +menu of the index view. You can also type "Control-L" (for list) from +either view to add to the shopping list more quickly. + +4.b. What is the "pantry" for? + +The idea of the "pantry" list is to record ingredients that you do not +want to shop for. In my kitchen, for example, I almost never need to +buy any of the basic spices, or staples like flour, butter and sugar, +so I put them in my pantry list. + +The reason these are added to a list rather than simply deleted from +your shopping list is so that you can always see what ingredients a +recipe calls for -- for the rare cases when you've run out of a staple +that's usually in your pantry. + +The pantry list is *not* designed to list all the contents of your +pantry. + +Only the items on your shopping list will be printed when you print a +shopping list. If you don't want something to be included on the +shopping list, drag it to your pantry list. If you do want it +included, drag it back form the pantry list to the shopping list. In +either case, Gourmet will remember what you did for next time. + +4.c. How can I delete items from my shopping list? + +Drag them to the pantry list. Or use the "remove from shopping list" +button on the toolbar. Or type "Control-D" while the item is selected. +The idea of the "pantry" is just a metaphor -- you should put anything +you don't want on your shopping list into the pantry list, regardless +of whether you actually have those items. + +4.d. So how can I move something from my pantry back onto my shopping list. + +Drag the item from the pantry list to the shopping list. Or use the +button on the toolbar. Or type "Control-B" ("B" for Back) while the +item is selected. + +5. Entering and Editing recipes + +5.a. How do I edit recipes + +When you create a recipe card, you will see the recipe editor window, +a window with multiple tabs. Each tab allows you to edit a different +aspect of the recipe (ingredients, instrucitons, etc.) + +When you open a previously created recipe, you'll see a simpler +"Recipe Card" display that puts all the information you need on one +screen. If you want to edit any part of this display, click on the +"Edit" button next to the relevant section. For example, next to the +"Ingredients" header, there's a "Edit ingredients" button which will +open the edit window on the ingredients tab. + +5.b. How do I convert units? + +Just edit the unit by clicking on the "unit" cell in your ingredient +list. Gourmet will then offer you a choice between simply changing the +unit and converting the item. + +If you select 1/2 cup milk, for example, and change "cup" to "g.", +Gourmet will ask you whether you want to convert it (121.69 grams) or +simply change the unit (1/2 g.). Although Gourmet does know the +density of milk (allowing it to calculate this example), it can't +always do weight-to-volume conversions! + +If you would like a manual unit calculator, you can activate the "Unit +Converter" plugin, which will make a handy unit calculator accessible +in the "Tools" menu. + +5.c. What about OR ingredients - this ingredient OR that? + +You can replicate the effect of a recipe calling for one ingredient or +another by making both of the ingredients you want to choose between +optional. + +When you go to add the recipe to a shopping list, you'll be +given an option of which items to add, and you can choose. + +This isn't perfect of course, because it allows you to add both or +neither ingredient, which is not what the recipe calls for! However, I +haven't thought of a more elegant way to allow for "or" items in +ingredient lists yet. + +6. How do I save recipes in a menu? + +Gourmet allows you to add a recipe to any other recipe as an +ingredients by pressing on the "Add recipe as ingredient" button in +the "Ingredients" tab of a recipe editor. Using this feature, you can +create a menu by creating a "New Recipe" and then adding as many +recipes as "ingredients" as you like. + +You can also activate the "Shopping List Saver" plugin (under Tools) +to allow you to save everything you've added to your shopping list as +a recipe for future use. + +A frequent request is to have gourmet support saving collections of +recipes as a menu or daily/weekly/monthly plan. This has yet to be +implemented. + +7. What are plugins? + +Gourmet strives to provide a simple basic interface for all users +while allowing powerful features to be accessed by users who are +interested in them. Plugins in Gourmet allow access to more powerful +features. + +Those plugins that are unlikely to "get in the way" of the user, such +as different import filters, are activated by default. Those plugins +that may only annoy some users, such as nutritional information, are +disabled by default. To activate or deactivate plugins, select +"Plugins" from the "Settings" menu. + +Note that plugins can also fail for various reasons. Sometimes, for +example, a plugin will require another software library that you may +not have installed. Gourmet tries to give you useful information when +it encounters a situation like this. At any rate, if a plugin does not +load correctly, the rest of Gourmet should still be able to run +without issue. + +8. Can I calculate nutritional information with Gourmet? + +Yes! Gourmet includes a copy of the USDA nutritional database, which +has thousands of items. For now, Gourmet doesn't know about any +ingredients by default--unless you tell it about your ingredients, +Gourmet will always say it is "missing nutritional information" for +the items in your recipe. If you care about nutritional information, +click "Edit" and a new window will pop up and ask you to give it the +information it needs to calculate nutritional information, either +using the USDA database foods or entering nutritional information by +hand if necessary. + +9. What are ingredient "key"s? + +By default, ingredient "keys" are not shown to the user and are not +needed for Gourmet. However, users interested in advanced features, +such as customizing nutritional information or shopping lists, will +want to activate the plugins that allow for viewing and editing +ingredient keys. + +Ingredient "keys" are the standardized name for an ingredient that +will go on your shopping list and, in future versions of gourmet, be +used to look up nutritional information. + +An ingredient like "Tomatoes, seeded and finely chopped" should be +keyed as "tomato" or "tomato, red" -- whatever you want to appear on +your shopping list. + +Standardizing keys means your shopping list can properly combine +ingredients from various recipes, understanding that "Tomatoes, seeded +and finely chopped", "ripe tomatoes", and "3 tomatoes, in slices", are +all the same thing. If this doesn't matter to you, don't worry about +making your ingredient keys standard. + +9.a. Is there a fast way to fix up ingredient keys? + +Yes. The "Key Editor" plugin (under "Tools") allows you to browse and search +all the ingredient keys in your database. This way, you can search for +all instances of "pepper" and standardize "pepper", "pepper, black" +and "black pepper" to one key. + +You can also see what recipe different keys are used in, so that if +you see something is obviously wrong, you can go correct it. + +Finally, the Key Editor allows you to easily change amounts and units +as well, either for individual instances or en masse. Using the Key +Editor, you can easily change all instances of ts. to tsp., or you +could tell it that for water, you'd like to change all instances of 1 +kg. to 1 liter. + +10. Can I do fancy printing with Gourmet? + +When you print from Gourmet, you can choose a variety of useful settings +under the "Page Layout" tab of the print dialog. This will allow you to +choose the format you want to print in (index cards, columns, etc.) and +the base font size you would like to use. + +11. Where does Gourmet store its data (including my recipes)? + +On Linux, Gourmet stores its data by default in ~/.gourmet (i.e. a hidden +subfolder of your home directory named .gourmet -- mind the dot). +On Windows, the gourmet directory (without the dot) is buried somewhere +in your user directory (normally \AppData\Roaming\gourmet). +If you can't find it, use Windows Explorer to search it within your user +directory. + +You can change that location by use of the `--gourmet-directory` command +line option. (Try `gourmet --help` for a list of all available options.) + +Also note the `--database-url` option which allows specifying an +[SQLAlchemy-style database URL](http://docs.sqlalchemy.org/en/rel_0_8/core/engines.html#database-urls) +(which by default is something like `sqlite:///~/.gourmet/recipes.db`, but +could also be e.g. `mysql://gourmetrecipe:password@localhost/gourmetrecipe` +if you're using a MySQL database, which by the way could also reside on some +remote server.) + +12. How can I change the font size? On my high resolution display, +I can hardly read the text that is being displayed. + +Within a Gnome environment, Gourmet respects the desktop-wide preferences. +On Windows, you can download a program that lets you tweak GTK+ settings and +change the font size. It's called "Change GTK2 Appearance" and can be found +at http://gtk-win.sourceforge.net/home/index.php/Main/GTKPreferenceTool + +13. Further Questions + +13.a. What if I have a good idea for how to improve Gourmet? + +Please submit a an issue tagged with the 'feature-request' label (and possibly +other applicable labels) at the following website: +https://github.com/thinkle/gourmet/issues + +13.b. What if I find a bug? + +Please submit an issue tagged with the 'bug' label (and possibly other +applicable labels) at the following website: +https://github.com/thinkle/gourmet/issues + +13.c. What if I have a question not answered here? + +Post your question in the Gourmet Help Forum here: +https://answers.launchpad.net/gourmet +Or, if for some reason that won't work, feel free to e-mail me at: +Thomas_Hinkle@alumni.brown.edu diff --git a/src/gourmet/data/LICENSE b/src/gourmet/data/LICENSE new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/src/gourmet/data/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/src/gourmet/gglobals.py b/src/gourmet/gglobals.py index 7e021e908..44340cef5 100644 --- a/src/gourmet/gglobals.py +++ b/src/gourmet/gglobals.py @@ -31,7 +31,6 @@ style_dir = os.path.join(settings.data_dir, 'style') icondir = os.path.join(settings.icon_base, '48x48', 'apps') -doc_base = settings.doc_base plugin_base = settings.plugin_base # GRAB PLUGIN DIR FOR HTML IMPORT diff --git a/src/gourmet/gtk_extras/dialog_extras.py b/src/gourmet/gtk_extras/dialog_extras.py index e0415e420..c829dc2ad 100644 --- a/src/gourmet/gtk_extras/dialog_extras.py +++ b/src/gourmet/gtk_extras/dialog_extras.py @@ -5,6 +5,7 @@ import xml.sax.saxutils from gettext import gettext as _ from pathlib import Path +from pkgutil import get_data as _get_data from typing import List, Optional from gi.repository import GObject, Gtk, Pango @@ -15,6 +16,7 @@ from . import optionTable + H_PADDING=12 Y_PADDING=12 @@ -615,7 +617,6 @@ class SimpleFaqDialog (ModalDialog): NESTED_MATCHER = re.compile("^[0-9]+[.][A-Za-z0-9.]+ .*") def __init__ (self, - faq_file='/home/tom/Projects/grm-0.8/FAQ', title="Frequently Asked Questions", jump_to = None, parent=None, @@ -639,7 +640,7 @@ def __init__ (self, self.index_dic = {} self.text = "" - self.parse_faq(faq_file) + self.parse_faq() if self.index_lines: self.paned = Gtk.Paned() self.indexView = Gtk.TreeView() @@ -673,34 +674,31 @@ def jump_to_header (self, text): self.indexView.expand_row(mod.get_path(itr),True) return - def parse_faq(self, filename: str) -> None: - """Parse file infile as our FAQ to display. - - infile can be a filename or a file-like object. - We parse index lines according to self.INDEX_MATCHER - """ + def parse_faq(self) -> None: + """Parse the FAQ for display.""" # Clear data self.index_lines = [] self.index_dic = {} self.text = "" - with open(filename, 'r') as fin: - for l in fin.readlines(): - line = l.strip() - if self.INDEX_MATCHER.match(line): # it is a heading - self.index_lines.append(line) - curiter = self.textbuf.get_iter_at_mark(self.textbuf.get_insert()) - self.index_dic[line] = self.textbuf.create_mark(None, - curiter, - left_gravity=True) - self.textbuf.insert_with_tags(curiter, - line + " ", - self.boldtag) - elif line: # it is body content - self.textbuf.insert_at_cursor(line + " ") - else: # an empty line is a paragraph break - self.textbuf.insert_at_cursor("\n\n") + faq = _get_data('gourmet', 'data/FAQ').decode() + assert faq + for l in faq.split('\n'): + line = l.strip() + if self.INDEX_MATCHER.match(line): # it is a heading + self.index_lines.append(line) + curiter = self.textbuf.get_iter_at_mark(self.textbuf.get_insert()) + self.index_dic[line] = self.textbuf.create_mark(None, + curiter, + left_gravity=True) + self.textbuf.insert_with_tags(curiter, + line + " ", + self.boldtag) + elif line: # it is body content + self.textbuf.insert_at_cursor(line + " ") + else: # an empty line is a paragraph break + self.textbuf.insert_at_cursor("\n\n") def setup_index (self): """Set up a clickable index view""" @@ -1161,8 +1159,8 @@ def getRadio (*args,**kwargs): d=RadioDialog(*args,**kwargs) return d.run() -def show_faq (*args,**kwargs): - d=SimpleFaqDialog(*args,**kwargs) +def show_faq(*args, **kwargs): + d = SimpleFaqDialog(*args, **kwargs) return d.run() def get_ratings_conversion (*args,**kwargs): @@ -1228,7 +1226,7 @@ def msg(*args): StarGenerator(), )], ['show dialog (not modal)',lambda *args: PreferencesDialog(options=opts,apply_func=show_options).show()], - ['show FAQ',lambda *args: show_faq(jump_to='shopping')], + ['show FAQ',lambda *args: show_faq(parent=w, jump_to='shopping')], ['show message',lambda *args: show_message('howdy',label='Hello there. This is a very long label for the top of a dialog.', sublabel='And this is a sub message.',message_type=Gtk.MessageType.WARNING)], ['get entry', lambda *args: getEntry(label='Main label',sublabel='sublabel',entryLabel='Entry Label: ')], ['get number', lambda *args: getNumber(label='Main label',sublabel='sublabel')], diff --git a/src/gourmet/reccard.py b/src/gourmet/reccard.py index ace2337b9..83f75bb4e 100644 --- a/src/gourmet/reccard.py +++ b/src/gourmet/reccard.py @@ -16,7 +16,7 @@ from gourmet.exporters.printer import PrintManager from gourmet.gdebug import debug from gourmet.gglobals import (FLOAT_REC_ATTRS, INT_REC_ATTRS, REC_ATTR_DIC, - REC_ATTRS, doc_base, imagedir, uibase) + REC_ATTRS, uibase, imagedir) from gourmet.gtk_extras import WidgetSaver # noqa: imports needed for glade from gourmet.gtk_extras import cb_extras as cb from gourmet.gtk_extras import dialog_extras as de @@ -236,7 +236,10 @@ def setup_actions (self): ('Preferences',Gtk.STOCK_PREFERENCES,None, None,None,self.preferences_cb), ('Help',Gtk.STOCK_HELP,_('_Help'), - None,None,lambda *args: de.show_faq(os.path.join(doc_base,'FAQ'),jump_to='Entering and Editing recipes')), + None,None, + lambda *args: de.show_faq(parent=self.window, + jump_to='Entering and Editing recipes') + ), ] ) self.recipeDisplayActionGroup.add_toggle_actions([ diff --git a/src/gourmet/settings.py b/src/gourmet/settings.py index 24453f9ee..ae12e6b79 100644 --- a/src/gourmet/settings.py +++ b/src/gourmet/settings.py @@ -12,7 +12,6 @@ ui_base = op.join(op.dirname(__file__), 'ui') ui_base = flatpak_ui if op.exists(flatpak_ui) else ui_base -doc_base = op.join(base_dir) locale_base = op.join(base_dir, 'build', 'mo') plugin_base = op.join(base_dir, 'build', 'share', 'gourmet') diff --git a/src/gourmet/shopgui.py b/src/gourmet/shopgui.py index a2812c4e4..6e236a23d 100644 --- a/src/gourmet/shopgui.py +++ b/src/gourmet/shopgui.py @@ -13,7 +13,6 @@ from . import convert, plugin, plugin_loader, prefs, reccard, recipeManager from .exporters.printer import PrintManager from .gdebug import debug -from .gglobals import doc_base from .gtk_extras import WidgetSaver from .gtk_extras import dialog_extras as de from .gtk_extras import fix_action_group_importance, mnemonic_manager @@ -684,7 +683,8 @@ def setup_actions (self): ), ('File',None,_('_File')), ('Help',Gtk.STOCK_HELP,_('_Help'),None,None, - lambda *args: de.show_faq(os.path.join(doc_base,'FAQ'),jump_to='Shopping')), + lambda *args: de.show_faq(parent=self.w, jump_to='Shopping') + ), ('HelpMenu',None,_('_Help')), ]) self.mainActionGroup.add_toggle_actions([ From 7f26805ac5b43d77d728a2f010bed7dfda29286e Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Mon, 2 Nov 2020 13:49:23 -0600 Subject: [PATCH 02/12] Replace icondir/icon_base with resources --- src/gourmet/GourmetRecipeManager.py | 10 ++++++---- src/gourmet/data/{icons => images}/gourmet.ico | Bin .../data/{icons/48x48/apps => images}/gourmet.png | Bin .../{icons/scalable/apps => images}/gourmet.svg | 0 src/gourmet/gglobals.py | 1 - src/gourmet/image_utils.py | 7 +++++++ src/gourmet/settings.py | 2 -- 7 files changed, 13 insertions(+), 7 deletions(-) rename src/gourmet/data/{icons => images}/gourmet.ico (100%) rename src/gourmet/data/{icons/48x48/apps => images}/gourmet.png (100%) rename src/gourmet/data/{icons/scalable/apps => images}/gourmet.svg (100%) diff --git a/src/gourmet/GourmetRecipeManager.py b/src/gourmet/GourmetRecipeManager.py index 8560990b5..82c859a27 100644 --- a/src/gourmet/GourmetRecipeManager.py +++ b/src/gourmet/GourmetRecipeManager.py @@ -16,8 +16,7 @@ from gourmet.exporters.exportManager import ExportManager from gourmet.exporters.printer import PrintManager from gourmet.gdebug import debug -from gourmet.gglobals import (DEFAULT_HIDDEN_COLUMNS, REC_ATTRS, - icondir, uibase) +from gourmet.gglobals import (DEFAULT_HIDDEN_COLUMNS, REC_ATTRS, uibase) from gourmet.gtk_extras import WidgetSaver from gourmet.gtk_extras import dialog_extras as de from gourmet.gtk_extras import (fix_action_group_importance, mnemonic_manager, @@ -29,6 +28,9 @@ get_thread_manager_gui) from gourmet.timer import show_timer +from .image_utils import load_pixbuf_from_resource as _load_pixbuf_from_resource + + UNDO = 1 SHOW_TRASH = 2 @@ -348,7 +350,7 @@ def show_about (self, *args): else: translator = defaults.CREDITS - logo=GdkPixbuf.Pixbuf.new_from_file(os.path.join(icondir,"gourmet.png")) + logo = _load_pixbuf_from_resource('gourmet.svg') # load LICENSE text file license_text = _get_data('gourmet', 'data/LICENSE').decode() @@ -952,7 +954,7 @@ def selection_changed (self, selected=False): def setup_main_window(self): self.window = self.app = Gtk.Window() - self.window.set_icon_from_file(os.path.join(icondir, 'gourmet.png')) + self.window.set_icon(_load_pixbuf_from_resource('gourmet.svg')) saver = WidgetSaver.WindowSaver( self.window, self.prefs.get('app_window', {'window_size': (800, 600)}) diff --git a/src/gourmet/data/icons/gourmet.ico b/src/gourmet/data/images/gourmet.ico similarity index 100% rename from src/gourmet/data/icons/gourmet.ico rename to src/gourmet/data/images/gourmet.ico diff --git a/src/gourmet/data/icons/48x48/apps/gourmet.png b/src/gourmet/data/images/gourmet.png similarity index 100% rename from src/gourmet/data/icons/48x48/apps/gourmet.png rename to src/gourmet/data/images/gourmet.png diff --git a/src/gourmet/data/icons/scalable/apps/gourmet.svg b/src/gourmet/data/images/gourmet.svg similarity index 100% rename from src/gourmet/data/icons/scalable/apps/gourmet.svg rename to src/gourmet/data/images/gourmet.svg diff --git a/src/gourmet/gglobals.py b/src/gourmet/gglobals.py index 44340cef5..baac8da25 100644 --- a/src/gourmet/gglobals.py +++ b/src/gourmet/gglobals.py @@ -30,7 +30,6 @@ imagedir = os.path.join(settings.data_dir, 'images') style_dir = os.path.join(settings.data_dir, 'style') -icondir = os.path.join(settings.icon_base, '48x48', 'apps') plugin_base = settings.plugin_base # GRAB PLUGIN DIR FOR HTML IMPORT diff --git a/src/gourmet/image_utils.py b/src/gourmet/image_utils.py index 73107bf77..3ea48ccd0 100644 --- a/src/gourmet/image_utils.py +++ b/src/gourmet/image_utils.py @@ -2,6 +2,7 @@ from collections import defaultdict from enum import Enum from pathlib import Path +from pkgutil import get_data as _get_data from typing import Dict, List, Optional from urllib.parse import unquote, urlparse @@ -87,6 +88,12 @@ def image_to_bytes(image: Image.Image) -> bytes: return ofi.getvalue() +def load_pixbuf_from_resource(resource_name: str) -> Pixbuf: + data = _get_data('gourmet', f'data/images/{resource_name}') + assert data + return bytes_to_pixbuf(data) + + def pixbuf_to_image(pixbuf: Pixbuf) -> Image.Image: data = pixbuf.get_pixels() width = pixbuf.props.width diff --git a/src/gourmet/settings.py b/src/gourmet/settings.py index ae12e6b79..e590d0414 100644 --- a/src/gourmet/settings.py +++ b/src/gourmet/settings.py @@ -19,5 +19,3 @@ # getting rid of indentations in this file which throws a syntax error # on install if getattr(sys, 'frozen', False): base_dir = op.dirname(sys.executable); data_dir = base_dir; ui_base = op.join(base_dir, 'ui'); doc_base = op.join(base_dir, 'doc'); locale_base = op.join(base_dir, 'locale'); plugin_base = op.join(base_dir) - -icon_base = op.join(data_dir, 'icons') From fe997b74e229bf3926100baee23d83593f795ea7 Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Mon, 2 Nov 2020 14:03:05 -0600 Subject: [PATCH 03/12] Remove obsolete html_plugin_dir This option and setting appear to been removed since at least commit 648a40f690554506411ed966fd69ee908533a7b8 in October 2008. --- src/gourmet/gglobals.py | 14 -------------- src/gourmet/optionparser.py | 5 ----- 2 files changed, 19 deletions(-) diff --git a/src/gourmet/gglobals.py b/src/gourmet/gglobals.py index baac8da25..55fdb3e4f 100644 --- a/src/gourmet/gglobals.py +++ b/src/gourmet/gglobals.py @@ -32,20 +32,6 @@ plugin_base = settings.plugin_base -# GRAB PLUGIN DIR FOR HTML IMPORT -if args.html_plugin_dir: - html_plugin_dir = args.html_plugin_dir -else: - html_plugin_dir = os.path.join(gourmetdir, 'html_plugins') - if not os.path.exists(html_plugin_dir): - os.makedirs(html_plugin_dir) - template_file = os.path.join(settings.data_dir, 'RULES_TEMPLATE') - if os.path.exists(template_file): - import shutil - shutil.copy(template_file, - os.path.join(html_plugin_dir, 'RULES_TEMPLATE') - ) - REC_ATTRS = [('title', _('Title'), 'Entry'), ('category', _('Category'), 'Combo'), ('cuisine', _('Cuisine'), 'Combo'), diff --git a/src/gourmet/optionparser.py b/src/gourmet/optionparser.py index 12d90db55..8eda4c770 100644 --- a/src/gourmet/optionparser.py +++ b/src/gourmet/optionparser.py @@ -14,11 +14,6 @@ dest='db_url', help='Database uri formatted like driver://path/to/db', default='') -parser.add_argument('--plugin-directory', - action='store', - dest='html_plugin_dir', - help='Directory for webpage import filter plugins.', - default='') parser.add_argument('--use-threads', action='store_const', const=True, From 499d18eb38333a84767bd441a2e3a0c2a4fe24ca Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Mon, 2 Nov 2020 15:50:27 -0600 Subject: [PATCH 04/12] Replace style_dir with resources --- src/gourmet/exporters/exportManager.py | 4 - src/gourmet/gglobals.py | 1 - .../epub_plugin/epub_exporter.py | 23 ++++-- .../html_plugin/html_exporter.py | 77 +++++++++++-------- 4 files changed, 59 insertions(+), 46 deletions(-) diff --git a/src/gourmet/exporters/exportManager.py b/src/gourmet/exporters/exportManager.py index 42bb30652..3c423dd9a 100644 --- a/src/gourmet/exporters/exportManager.py +++ b/src/gourmet/exporters/exportManager.py @@ -89,9 +89,6 @@ def do_single_export (self, rec, filename, exp_type, mult=1, extra_prefs=EXTRA_P def offer_multiple_export (self, recs, prefs, parent=None, prog=None, export_all=False): """Offer user a chance to export multiple recipes at once. - - Return the exporter class capable of doing this and a - dictionary of arguments for the progress dialog. """ if (not export_all) or (len(recs) < 950): # inelegantly avoid bug that happens when this code runs @@ -124,7 +121,6 @@ def offer_multiple_export (self, recs, prefs, parent=None, prog=None, message_type=Gtk.MessageType.ERROR, ) return - return instance def get_extra_prefs (self, myexp, extra_prefs): if extra_prefs == EXTRA_PREFS_AUTOMATIC: diff --git a/src/gourmet/gglobals.py b/src/gourmet/gglobals.py index 55fdb3e4f..38c3d36b7 100644 --- a/src/gourmet/gglobals.py +++ b/src/gourmet/gglobals.py @@ -28,7 +28,6 @@ # note: this stuff must be kept in sync with changes in setup.py data_dir = settings.data_dir imagedir = os.path.join(settings.data_dir, 'images') -style_dir = os.path.join(settings.data_dir, 'style') plugin_base = settings.plugin_base diff --git a/src/gourmet/plugins/import_export/epub_plugin/epub_exporter.py b/src/gourmet/plugins/import_export/epub_plugin/epub_exporter.py index 0bf184a95..6c47489b6 100644 --- a/src/gourmet/plugins/import_export/epub_plugin/epub_exporter.py +++ b/src/gourmet/plugins/import_export/epub_plugin/epub_exporter.py @@ -1,14 +1,16 @@ -import os import re import xml.sax.saxutils from gettext import gettext as _ +from pkgutil import get_data as _get_data from string import Template +from typing import Optional from ebooklib import epub from gourmet import convert, gglobals from gourmet.exporters.exporter import ExporterMultirec, exporter_mult + RECIPE_HEADER = Template(''' @@ -20,14 +22,16 @@ ''') RECIPE_FOOT= "" -EPUB_DEFAULT_CSS="epubdefault.css" - class EpubWriter(): """This class contains all things to write an epub and is a small wrapper around the EbookLib which is capable of producing epub files and maybe kindle in the future (it is under heavy development). """ + + _default_style = 'epubdefault.css' + + def __init__(self, outFileName): """ @param outFileName The filename + path the ebook is written to on finish. @@ -57,7 +61,7 @@ def __init__(self, outFileName): # This adds the field also known as keywords in some programs. self.ebook.add_metadata('DC', 'subject', "cooking") - def addRecipeCssFromFile(self, filename): + def addRecipeCssFromFile(self, filename: Optional[str] = None) -> str: """ Adds the CSS file from filename to the book. The style will be added to the books root. @param filename The file the css is read from and attached @@ -65,14 +69,19 @@ def addRecipeCssFromFile(self, filename): """ cssFileName = "Style/recipe.css" - style = open(filename, 'rb').read() + if filename: + with open(filename, 'rb') as fh: + style = fh.read() + else: + style = _get_data('gourmet', f'data/style/{self._default_style}') + assert style recipe_css = epub.EpubItem( uid="style" , file_name=cssFileName , media_type="text/css" , content=style) self.ebook.add_item(recipe_css) self.recipeCss = recipe_css - return cssFileName; + return cssFileName def addJpegImage(self, imageData): """Adds a jpeg image from the imageData array to the book and returns @@ -270,7 +279,7 @@ def write_foot (self): class website_exporter (ExporterMultirec): def __init__ (self, rd, recipe_table, out, conv=None, ext='epub', copy_css=True, - css=os.path.join(gglobals.style_dir,EPUB_DEFAULT_CSS), + css: Optional[str] = None, index_rows=['title','category','cuisine','rating','yields'], change_units=False, mult=1): diff --git a/src/gourmet/plugins/import_export/html_plugin/html_exporter.py b/src/gourmet/plugins/import_export/html_plugin/html_exporter.py index 8b0395dbd..cac82e706 100644 --- a/src/gourmet/plugins/import_export/html_plugin/html_exporter.py +++ b/src/gourmet/plugins/import_export/html_plugin/html_exporter.py @@ -9,10 +9,13 @@ import urllib.request import xml.sax.saxutils from gettext import gettext as _ +from pkgutil import get_data as _get_data +from typing import Optional from gourmet import convert, gglobals from gourmet.exporters.exporter import ExporterMultirec, exporter_mult + HTML_HEADER_START = """ @@ -21,9 +24,19 @@ """ +def _read_css(filename: Optional[str] = None) -> str: + if filename: + with open(filename, 'r') as fh: + style = fh.read() + else: + style = _get_data('gourmet', 'data/style/default.css').decode() + assert style + return style + + class html_exporter (exporter_mult): def __init__ (self, rd, r, out, conv=None, - css=os.path.join(gglobals.style_dir,"default.css"), + css: Optional[str] = None, embed_css=True, start_html=True, end_html=True, imagedir="pics/", imgcount=1, link_generator=None, # exporter_mult args @@ -42,8 +55,9 @@ def __init__ (self, rd, r, out, conv=None, or None if it can't reference the recipe based on the ID.""" self.start_html=start_html self.end_html=end_html - self.embed_css=embed_css - self.css=css + self._css_file = css + self._css = _read_css(css) + self._embed_css = embed_css if css else True self.link_generator=link_generator if imagedir and imagedir[-1] != os.path.sep: imagedir += os.path.sep #make sure we end w/ slash if not imagedir: imagedir = "" #make sure it's a string @@ -74,16 +88,14 @@ def write_head (self): if self.start_html: self.out.write(HTML_HEADER_START) self.out.write("%s"%self.get_title()) - if self.css: - if self.embed_css: - self.out.write("") - else: - self.out.write(""%self.make_relative_link(self.css)) + if self._embed_css: + self.out.write("") + else: + assert self._css_file + link = self.make_relative_link(self._css_file) + self.out.write(f"") self.out.write(HTML_HEADER_CLOSE) self.out.write('') self.out.write('
') @@ -208,35 +220,33 @@ def make_relative_link (self, filename): class website_exporter (ExporterMultirec): def __init__ (self, rd, recipe_table, out, conv=None, ext='htm', copy_css=True, - css=os.path.join(gglobals.style_dir,'default.css'), + css: Optional[str] = None, imagedir='pics' + os.path.sep, index_rows=['title','category','cuisine','rating','yields'], change_units=False, mult=1): self.ext=ext - self.css=css - self.embed_css = False + self._css_file, self._css = _read_css(css) if copy_css: - styleout = os.path.join(out,'style.css') + styleout = os.path.join(out, 'style.css') if not os.path.isdir(out): os.makedirs(out) - to_copy = open(self.css,'r') - print('writing css to ',styleout) - to_paste = open(styleout,'w') - to_paste.write(to_copy.read()) - to_copy.close(); to_paste.close() - self.css = styleout + print('writing css to ', styleout) + with open(styleout, 'w') as fh: + fh.write(self._css) + self._css_file = styleout + self._embed_css = not self._css_file self.imagedir=imagedir self.index_rows=index_rows self.imgcount=1 self.added_dict={} self.exportargs={'embed_css': False, - 'css': self.css, - 'imgcount': self.imgcount, - 'imagedir':self.imagedir, + 'css': self._css_file, + 'imgcount': self.imgcount, + 'imagedir': self.imagedir, 'link_generator': self.generate_link, - 'change_units':change_units, - 'mult':mult} + 'change_units': change_units, + 'mult': mult} if conv: self.exportargs['conv']=conv ExporterMultirec.__init__(self, rd, recipe_table, out, @@ -250,15 +260,14 @@ def write_header (self): self.indexf = open(self.indexfn,'w') self.indexf.write(HTML_HEADER_START) self.indexf.write("Recipe Index") - if self.embed_css: + if self._embed_css: self.indexf.write("") else: - self.indexf.write(""%self.make_relative_link(self.css)) + assert self._css_file + link = self.make_relative_link(self._css_file) + self.indexf.write(f"") self.indexf.write(HTML_HEADER_CLOSE) self.indexf.write('') self.indexf.write('
\n') From 0c566114e7643556595c797943f989aebbefae0e Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Mon, 2 Nov 2020 17:52:02 -0600 Subject: [PATCH 05/12] Replace imagedir with resources --- src/gourmet/gglobals.py | 18 +++++++++--------- src/gourmet/reccard.py | 8 +++++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/gourmet/gglobals.py b/src/gourmet/gglobals.py index 38c3d36b7..6a47999e8 100644 --- a/src/gourmet/gglobals.py +++ b/src/gourmet/gglobals.py @@ -6,8 +6,10 @@ from gi.repository import Gdk, GdkPixbuf, Gtk from . import settings +from .image_utils import load_pixbuf_from_resource as _load_pixbuf_from_resource from .optionparser import args + uibase = os.path.join(settings.ui_base) lib_dir = os.path.join(settings.lib_dir) @@ -27,7 +29,6 @@ # note: this stuff must be kept in sync with changes in setup.py data_dir = settings.data_dir -imagedir = os.path.join(settings.data_dir, 'images') plugin_base = settings.plugin_base @@ -87,18 +88,18 @@ def build_rec_attr_dic(): icon_factory = Gtk.IconFactory() +# TODO: Move this into GTK-specific code? +# TODO: Update/remove potentially-deprecated code? +# GTK 3 has deprecated the use of stock icons, so this may need to be rewritten +# (or removed altogether) to ensure this works in the future def add_icon(file_name, stock_id, label=None, modifier=0, keyval=0): - pb = GdkPixbuf.Pixbuf.new_from_file(file_name) + pb = _load_pixbuf_from_resource(file_name) iconset = Gtk.IconSet.new_from_pixbuf(pb) icon_factory.add(stock_id, iconset) icon_factory.add_default() # TODO: fix adding icons return - Gtk.stock_add([(stock_id, - label, - modifier, - keyval, - "")]) + Gtk.stock_add([(stock_id, label, modifier, keyval, "")]) for filename, stock_id, label, modifier, keyval in [ @@ -112,8 +113,7 @@ def add_icon(file_name, stock_id, label=None, modifier=0, keyval=0): ('reccard_edit.png', 'edit-recipe-card', None, 0, 0), ]: - add_icon(os.path.join(imagedir, filename), stock_id, - label, modifier, keyval) + add_icon(filename, stock_id, label, modifier, keyval) # Color scheme preference diff --git a/src/gourmet/reccard.py b/src/gourmet/reccard.py index 83f75bb4e..5cb27b5bb 100644 --- a/src/gourmet/reccard.py +++ b/src/gourmet/reccard.py @@ -16,7 +16,7 @@ from gourmet.exporters.printer import PrintManager from gourmet.gdebug import debug from gourmet.gglobals import (FLOAT_REC_ATTRS, INT_REC_ATTRS, REC_ATTR_DIC, - REC_ATTRS, uibase, imagedir) + REC_ATTRS, uibase) from gourmet.gtk_extras import WidgetSaver # noqa: imports needed for glade from gourmet.gtk_extras import cb_extras as cb from gourmet.gtk_extras import dialog_extras as de @@ -32,6 +32,8 @@ RecEditorModule, RecEditorPlugin, ToolPlugin) from gourmet.recindex import RecIndex +from .image_utils import load_pixbuf_from_resource as _load_pixbuf_from_resource + def find_entry(w) -> Optional[Gtk.Entry]: if isinstance(w, Gtk.Entry): @@ -362,7 +364,7 @@ def reflow_on_allocate_cb (self, sw, allocation): # Main GUI setup def setup_main_window (self): self.window = Gtk.Window() - self.window.set_icon_from_file(os.path.join(imagedir,'reccard.png')) + self.window.set_icon(_load_pixbuf_from_resource('reccard.png')) self.window.connect('delete-event',self.hide) self.conf.append(WidgetSaver.WindowSaver(self.window, self.prefs.get('reccard_window_%s'%self.current_rec.id, @@ -949,7 +951,7 @@ def show_module(self, module_name: str) -> None: def setup_main_interface (self): self.window = Gtk.Window() - self.window.set_icon_from_file(os.path.join(imagedir,'reccard_edit.png')) + self.window.set_icon(_load_pixbuf_from_resource('reccard_edit.png')) title = ((self.current_rec and self.current_rec.title) or _('New Recipe')) + ' (%s)'%_('Edit') self.window.set_title(title) self.window.connect('delete-event', From 01c48f27d409a2fa2797118f4dedbf50f738e459 Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Tue, 3 Nov 2020 02:43:35 -0600 Subject: [PATCH 06/12] Replace data_dir with resources --- .../images/Nutrition.png | Bin src/gourmet/gglobals.py | 16 +++-- .../databaseGrabber.py | 63 +++++++----------- .../images/__init__.py | 0 .../nutritional_information/main_plugin.py | 9 ++- .../nutritionGrabberGui.py | 3 +- src/gourmet/settings.py | 1 - src/gourmet/sound.py | 9 ++- src/gourmet/timer.py | 21 ++++-- 9 files changed, 62 insertions(+), 60 deletions(-) rename src/gourmet/{plugins/nutritional_information => data}/images/Nutrition.png (100%) delete mode 100644 src/gourmet/plugins/nutritional_information/images/__init__.py diff --git a/src/gourmet/plugins/nutritional_information/images/Nutrition.png b/src/gourmet/data/images/Nutrition.png similarity index 100% rename from src/gourmet/plugins/nutritional_information/images/Nutrition.png rename to src/gourmet/data/images/Nutrition.png diff --git a/src/gourmet/gglobals.py b/src/gourmet/gglobals.py index 6a47999e8..b024eb6cc 100644 --- a/src/gourmet/gglobals.py +++ b/src/gourmet/gglobals.py @@ -28,8 +28,6 @@ # use_threads = False # note: this stuff must be kept in sync with changes in setup.py -data_dir = settings.data_dir - plugin_base = settings.plugin_base REC_ATTRS = [('title', _('Title'), 'Entry'), @@ -88,13 +86,17 @@ def build_rec_attr_dic(): icon_factory = Gtk.IconFactory() -# TODO: Move this into GTK-specific code? +# TODO: Move this into GTK-specific code # TODO: Update/remove potentially-deprecated code? # GTK 3 has deprecated the use of stock icons, so this may need to be rewritten # (or removed altogether) to ensure this works in the future -def add_icon(file_name, stock_id, label=None, modifier=0, keyval=0): - pb = _load_pixbuf_from_resource(file_name) - iconset = Gtk.IconSet.new_from_pixbuf(pb) +def add_icon( + pixbuf: GdkPixbuf.Pixbuf, + stock_id: str, + label: str = None, + modifier: Gdk.ModifierType = 0, + keyval: int = 0) -> None: + iconset = Gtk.IconSet.new_from_pixbuf(pixbuf) icon_factory.add(stock_id, iconset) icon_factory.add_default() # TODO: fix adding icons @@ -113,7 +115,7 @@ def add_icon(file_name, stock_id, label=None, modifier=0, keyval=0): ('reccard_edit.png', 'edit-recipe-card', None, 0, 0), ]: - add_icon(filename, stock_id, label, modifier, keyval) + add_icon(_load_pixbuf_from_resource(filename), stock_id, label, modifier, keyval) # Color scheme preference diff --git a/src/gourmet/plugins/nutritional_information/databaseGrabber.py b/src/gourmet/plugins/nutritional_information/databaseGrabber.py index 8446a244f..7c8371173 100644 --- a/src/gourmet/plugins/nutritional_information/databaseGrabber.py +++ b/src/gourmet/plugins/nutritional_information/databaseGrabber.py @@ -1,12 +1,9 @@ -import os.path import re -import sys import tempfile -import urllib.error -import urllib.parse import urllib.request import zipfile from gettext import gettext as _ +from pkgutil import get_data as _get_data from gourmet.gdebug import TimeAction @@ -66,47 +63,35 @@ def get_file_from_url (self, filename): tofi2.seek(0) return tofi2 - def get_abbrev (self, filename=None): - if filename: - afi = open(filename,'r') - else: - afi = self.get_file_from_url(self.ABBREV_FILE_NAME) - self.parse_abbrevfile(afi) - afi.close() + def get_abbrev(self) -> None: + abbreviations = _get_data('gourmet', f'data/{self.ABBREV_FILE_NAME}') + assert abbreviations + self.parse_abbrevfile(abbreviations) del self.foodgroups_by_ndbno - def get_groups (self, filename=None): + def get_groups(self) -> None: self.group_dict = {} - if filename: - afi = open(filename,'r') - else: - afi = self.get_file_from_url(self.DESC_FILE_NAME) self.foodgroups_by_ndbno = {} - for l in afi.readlines(): + + # TODO: Convert FOOD_DES.txt to UTF-8 + groups = _get_data('gourmet', f'data/{self.DESC_FILE_NAME}').decode('iso-8859-1') + assert groups + for l in groups.splitlines(): flds = l.split('^') ndbno = int(flds[0].strip('~')) grpno = int(flds[1].strip('~')) self.foodgroups_by_ndbno[ndbno] = grpno - def get_weight (self, filename=None): - if filename: - wfi = open(filename,'r') - else: - wfi = self.get_file_from_url(self.WEIGHT_FILE_NAME) - self.parse_weightfile(wfi) - wfi.close() + def get_weight(self) -> None: + weights = _get_data('gourmet', f'data/{self.WEIGHT_FILE_NAME}') + assert weights + self.parse_weightfile(weights) - def grab_data (self, directory=None): + def grab_data(self) -> None: self.db.changed = True - self.get_groups((isinstance(directory,str) - and - os.path.join(directory,self.DESC_FILE_NAME))) - self.get_abbrev((isinstance(directory,str) - and - os.path.join(directory,self.ABBREV_FILE_NAME))) - self.get_weight((isinstance(directory,str) - and - os.path.join(directory,self.WEIGHT_FILE_NAME))) + self.get_groups() + self.get_abbrev() + self.get_weight() def parse_line (self, line, field_defs, split_on='^'): """Handed a line and field definitions, return a dictionary of @@ -154,11 +139,12 @@ def parse_abbrevfile (self, abbrevfile): if self.show_progress: self.show_progress(float(0.03),_('Parsing nutritional data...')) self.datafile = tempfile.TemporaryFile() - ll=abbrevfile.readlines() + ll=abbrevfile.splitlines() tot=len(ll) n = 0 for n,l in enumerate(ll): - l = str(l.decode('latin_1')) + # TODO: Convert ABBREV.txt to UTF-8 + l = str(l.decode('iso-8859-1')) tline=TimeAction('1 line iteration',2) t=TimeAction('split fields',2) d = self.parse_line(l,NUTRITION_FIELDS) @@ -193,11 +179,12 @@ def parse_abbrevfile (self, abbrevfile): def parse_weightfile (self, weightfile): if self.show_progress: self.show_progress(float(0.03),_('Parsing weight data...')) - ll=weightfile.readlines() + ll=weightfile.splitlines() tot=len(ll) n=0 for n,l in enumerate(ll): - l = str(l.decode('latin_1')) + # TODO: Convert WEIGHT.txt to UTF-8 + l = str(l.decode('iso-8859-1')) if self.show_progress and n % 50 == 0: self.show_progress( float(n)/tot, diff --git a/src/gourmet/plugins/nutritional_information/images/__init__.py b/src/gourmet/plugins/nutritional_information/images/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/gourmet/plugins/nutritional_information/main_plugin.py b/src/gourmet/plugins/nutritional_information/main_plugin.py index 98df5021f..285bcfffa 100644 --- a/src/gourmet/plugins/nutritional_information/main_plugin.py +++ b/src/gourmet/plugins/nutritional_information/main_plugin.py @@ -1,7 +1,7 @@ -import os.path from gettext import gettext as _ -from gourmet.gglobals import add_icon +from gourmet.gglobals import add_icon as _add_icon +from gourmet.image_utils import load_pixbuf_from_resource as _load_pixbuf_from_resource from gourmet.plugin import MainPlugin from . import nutrition, nutritionGrabberGui @@ -11,9 +11,8 @@ class NutritionMainPlugin (MainPlugin): def activate (self, pluggable): """Setup nutritional database stuff.""" - add_icon(os.path.join(os.path.split(__file__)[0],'images','Nutrition.png'), - 'nutritional-info', - _('Nutritional Information')) + pixbuf = _load_pixbuf_from_resource('Nutrition.png') + _add_icon(pixbuf, 'nutritional-info', _('Nutritional Information')) nutritionGrabberGui.check_for_db(pluggable.rd) pluggable.nd = nutrition.NutritionData(pluggable.rd,pluggable.conv) pluggable.rd.nd = pluggable.nd diff --git a/src/gourmet/plugins/nutritional_information/nutritionGrabberGui.py b/src/gourmet/plugins/nutritional_information/nutritionGrabberGui.py index 7a512c738..1e1866e61 100644 --- a/src/gourmet/plugins/nutritional_information/nutritionGrabberGui.py +++ b/src/gourmet/plugins/nutritional_information/nutritionGrabberGui.py @@ -6,7 +6,6 @@ from gi.repository import Gtk import gourmet.gtk_extras.dialog_extras as de -from gourmet.gglobals import data_dir from . import databaseGrabber @@ -41,7 +40,7 @@ def load_db (self): pause=self.pausecb, stop=self.stopcb) self.progdialog.show() - self.grab_data(data_dir) + self.grab_data() self.show_progress(1,_('Nutritonal database import complete!')) self.progdialog.set_response_sensitive(Gtk.ResponseType.OK,True) self.progdialog.hide() diff --git a/src/gourmet/settings.py b/src/gourmet/settings.py index e590d0414..054da300f 100644 --- a/src/gourmet/settings.py +++ b/src/gourmet/settings.py @@ -6,7 +6,6 @@ base_dir = op.abspath(op.join(op.dirname(__file__), '..')) lib_dir = op.join(base_dir, 'gourmet') -data_dir = op.join(lib_dir, 'data') flatpak_ui = "/app/share/gourmet/ui" ui_base = op.join(op.dirname(__file__), 'ui') diff --git a/src/gourmet/sound.py b/src/gourmet/sound.py index f1b308463..4a599f696 100644 --- a/src/gourmet/sound.py +++ b/src/gourmet/sound.py @@ -1,12 +1,17 @@ +from urllib.request import pathname2url as _pathname_to_url + from gi.repository import Gst class Player: + def __init__(self): Gst.init() self.player = Gst.ElementFactory.make('playbin', 'player') - def play_file(self, path: str): + + def play_file(self, filepath: str) -> None: + uri = _pathname_to_url(filepath) self.player.set_state(Gst.State.NULL) - self.player.set_property('uri', 'file://' + path) + self.player.set_property('uri', f'file://{uri}') self.player.set_state(Gst.State.PLAYING) diff --git a/src/gourmet/timer.py b/src/gourmet/timer.py index 26b4b3b8e..c2042887e 100644 --- a/src/gourmet/timer.py +++ b/src/gourmet/timer.py @@ -1,7 +1,10 @@ -import os +import os as _os +import os.path import time import xml.sax.saxutils from gettext import gettext as _ +from pkgutil import get_data as _get_data +from tempfile import mkstemp as _mkstemp from typing import Callable, List, Optional from gi.repository import GLib, Gtk @@ -188,10 +191,17 @@ def note_changed_cb (self, entry): def init_player (self): self.player = Player() - def play_tune (self): - sound_file = self.sounds_and_files[cb.cb_get_active_text(self.soundComboBox)] - sound_file = os.path.join(gglobals.data_dir,'sound',sound_file) - self.player.play_file(sound_file) + def play_tune (self) -> None: + sound = self.sounds_and_files[cb.cb_get_active_text(self.soundComboBox)] + data = _get_data('gourmet', f'data/sound/{sound}') + assert data + + # TODO!!! Delete the tempfile when we're done + # TODO: Figure out how to make GStreamer play raw bytes + fd, fname = _mkstemp('.ogg') + _os.write(fd, data) + _os.close(fd) + self.player.play_file(fname) def annoy_user (self): if self.keep_annoying: @@ -203,6 +213,7 @@ def timer_done_cb (self): self.play_tune() if self.repeatCheckButton.get_active(): self.keep_annoying = True + # TODO: Timeout on when the audio file actually stops playing GLib.timeout_add(3000, self.annoy_user) self.timerBox.hide() self.expander1.hide() From ea6884993a0a4db618a1684ec31a7d4ea5855689 Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Wed, 4 Nov 2020 14:27:37 -0600 Subject: [PATCH 07/12] Replace uibase with resources --- src/gourmet/GourmetRecipeManager.py | 8 +- src/gourmet/backends/DatabaseChooser.py | 4 +- src/gourmet/batchEditor.py | 4 +- src/gourmet/gglobals.py | 1 - src/gourmet/gtk_extras/mnemonic_manager.py | 5 +- .../plugins/field_editor/fieldEditor.py | 4 +- .../nutritionInfoEditor.py | 6 +- .../plugins/unit_converter/convertGui.py | 1 - src/gourmet/prefsGui.py | 10 +- src/gourmet/reccard.py | 12 +- src/gourmet/settings.py | 4 - src/gourmet/shopEditor.py | 5 +- src/gourmet/timer.py | 4 +- src/gourmet/ui/generic_importer.ui | 391 ------------------ .../nutritionDruid.ui | 0 15 files changed, 26 insertions(+), 433 deletions(-) delete mode 100644 src/gourmet/ui/generic_importer.ui rename src/gourmet/{plugins/nutritional_information => ui}/nutritionDruid.ui (100%) diff --git a/src/gourmet/GourmetRecipeManager.py b/src/gourmet/GourmetRecipeManager.py index 82c859a27..f972f4eaa 100644 --- a/src/gourmet/GourmetRecipeManager.py +++ b/src/gourmet/GourmetRecipeManager.py @@ -1,5 +1,3 @@ -import os -import os.path from pkgutil import get_data as _get_data import re import threading @@ -16,7 +14,7 @@ from gourmet.exporters.exportManager import ExportManager from gourmet.exporters.printer import PrintManager from gourmet.gdebug import debug -from gourmet.gglobals import (DEFAULT_HIDDEN_COLUMNS, REC_ATTRS, uibase) +from gourmet.gglobals import DEFAULT_HIDDEN_COLUMNS, REC_ATTRS from gourmet.gtk_extras import WidgetSaver from gourmet.gtk_extras import dialog_extras as de from gourmet.gtk_extras import (fix_action_group_importance, mnemonic_manager, @@ -503,7 +501,7 @@ def __init__ (self, rg): self.rg = rg self.rmodel = self.rg.rmodel self.ui=Gtk.Builder() - self.ui.add_from_file(os.path.join(uibase,'recipe_index.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/recipe_index.ui').decode()) RecIndex.__init__(self, self.ui, self.rg.rd, self.rg) self.setup_main_window() @@ -870,7 +868,7 @@ def __init__(self): self.setup_index_columns() self.setup_hacks() self.ui=Gtk.Builder() - self.ui.add_from_file(os.path.join(uibase,'recipe_index.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/recipe_index.ui').decode()) self.setup_actions() RecIndex.__init__(self, ui=self.ui, diff --git a/src/gourmet/backends/DatabaseChooser.py b/src/gourmet/backends/DatabaseChooser.py index 91d1f6705..f58c3c8e7 100644 --- a/src/gourmet/backends/DatabaseChooser.py +++ b/src/gourmet/backends/DatabaseChooser.py @@ -1,5 +1,6 @@ import os.path from gettext import gettext as _ +from pkgutil import get_data as _get_data from gi.repository import Gtk @@ -20,9 +21,8 @@ def __init__ (self, okcb=lambda x: debug(x,0), modal=True): self.default_file_directory = gglobals.gourmetdir self.default_files = {'sqlite':'recipes.db' } - uifile = os.path.join(gglobals.uibase,'databaseChooser.ui') self.ui = Gtk.Builder() - self.ui.add_from_file(uifile) + self.ui.add_from_string(_get_data('gourmet', 'ui/databaseChooser.ui').decode()) self.connection_widgets = ['hostEntry','userEntry','pwEntry','dbEntry', 'hostLabel','userLabel','pwLabel','dbLabel', 'pwCheckButton'] diff --git a/src/gourmet/batchEditor.py b/src/gourmet/batchEditor.py index 9f115e8f6..77d5d03c1 100644 --- a/src/gourmet/batchEditor.py +++ b/src/gourmet/batchEditor.py @@ -1,4 +1,4 @@ -import os +from pkgutil import get_data as _get_data from gi.repository import Gtk @@ -14,7 +14,7 @@ def __init__ (self, rg): def setup_ui (self): self.ui = Gtk.Builder() - self.ui.add_from_file(os.path.join(gglobals.uibase,'batchEditor.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/batchEditor.ui').decode()) self.dialog = self.ui.get_object('batchEditorDialog') self.setFieldWhereBlankButton = self.ui.get_object('setFieldWhereBlankButton') self.setup_boxes() diff --git a/src/gourmet/gglobals.py b/src/gourmet/gglobals.py index b024eb6cc..8eb560b12 100644 --- a/src/gourmet/gglobals.py +++ b/src/gourmet/gglobals.py @@ -10,7 +10,6 @@ from .optionparser import args -uibase = os.path.join(settings.ui_base) lib_dir = os.path.join(settings.lib_dir) gourmetdir: Path = Path(os.environ['HOME']).absolute() / '.gourmet' diff --git a/src/gourmet/gtk_extras/mnemonic_manager.py b/src/gourmet/gtk_extras/mnemonic_manager.py index e85f93c40..f9e9837f0 100644 --- a/src/gourmet/gtk_extras/mnemonic_manager.py +++ b/src/gourmet/gtk_extras/mnemonic_manager.py @@ -316,11 +316,10 @@ def change_mnemonic (self, widget, new_mnemonic): if __name__ == '__main__': - from gourmet import gglobals - import os.path + from pkgutil import get_data mm=MnemonicManager() ui = Gtk.Builder() - ui.add_from_file(os.path.join(gglobals.uibase,'app.ui')) + ui.add_from_string(get_data('gourmet', 'ui/app.ui').decode()) mm.add_builder(ui) #tree = ui.get_widget('recTree') #rend = Gtk.CellRendererText() diff --git a/src/gourmet/plugins/field_editor/fieldEditor.py b/src/gourmet/plugins/field_editor/fieldEditor.py index 94bb0de58..c106b3dd8 100644 --- a/src/gourmet/plugins/field_editor/fieldEditor.py +++ b/src/gourmet/plugins/field_editor/fieldEditor.py @@ -1,6 +1,6 @@ -import os.path from gettext import gettext as _ from gettext import ngettext +from pkgutil import get_data as _get_data from gi.repository import Gtk @@ -20,7 +20,7 @@ def __init__ (self, rd, rg): self.field = None; self.other_field = None self.rd = rd; self.rg = rg self.ui = Gtk.Builder() - self.ui.add_from_file(os.path.join(gglobals.uibase,'valueEditor.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/valueEditor.ui').decode()) self.__setup_widgets__() self.__setup_treeview__() self.ui.connect_signals({ diff --git a/src/gourmet/plugins/nutritional_information/nutritionInfoEditor.py b/src/gourmet/plugins/nutritional_information/nutritionInfoEditor.py index 3edcdaf73..6dcff370a 100644 --- a/src/gourmet/plugins/nutritional_information/nutritionInfoEditor.py +++ b/src/gourmet/plugins/nutritional_information/nutritionInfoEditor.py @@ -1,13 +1,12 @@ -import os import re from gettext import gettext as _ from gettext import ngettext +from pkgutil import get_data as _get_data import sqlalchemy from gi.repository import GObject, Gtk, Pango import gourmet.backends.db -import gourmet.gglobals as gglobals import gourmet.gtk_extras.cb_extras as cb import gourmet.gtk_extras.pageable_store as pageable_store @@ -22,7 +21,8 @@ def __init__ (self, rd, prefs=None, ui=None, self.ui = ui else: self.ui = Gtk.Builder() - self.ui.add_from_file(os.path.join(gglobals.uibase,'nutritionDruid.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/nutritionDruid.ui').decode()) + self.rd = rd self.prefs = prefs # Initialize variables used for search diff --git a/src/gourmet/plugins/unit_converter/convertGui.py b/src/gourmet/plugins/unit_converter/convertGui.py index 93f36e340..260811800 100644 --- a/src/gourmet/plugins/unit_converter/convertGui.py +++ b/src/gourmet/plugins/unit_converter/convertGui.py @@ -180,6 +180,5 @@ def close (self, *args): Gtk.main_quit() if __name__ == '__main__': - uibase="/home/tom/Projects/gourmet/glade/" cg=ConvGui() Gtk.main() diff --git a/src/gourmet/prefsGui.py b/src/gourmet/prefsGui.py index 14288bfcf..0fc48ca6d 100644 --- a/src/gourmet/prefsGui.py +++ b/src/gourmet/prefsGui.py @@ -1,8 +1,8 @@ -import os.path +from pkgutil import get_data as _get_data from gi.repository import Gtk -from . import gglobals, plugin, plugin_loader +from . import plugin, plugin_loader from .gtk_extras import optionTable @@ -22,8 +22,6 @@ class PreferencesGui (plugin_loader.Pluggable): def __init__ ( self, prefs, - uifile=os.path.join(gglobals.uibase, - 'preferenceDialog.ui'), radio_options={'shop_handle_optional':{'optional_ask':0, 'optional_add':1, 'optional_dont_add':-1 @@ -45,8 +43,6 @@ def __init__ ( ): """Set up our PreferencesGui - uifile points us to our UI file - radio_options is a dictionary of preferences controlled by radio buttons. {preference_name: {radio_widget: value, radio_widget: value, ...} @@ -60,7 +56,7 @@ def __init__ ( self.prefs = prefs self.ui = Gtk.Builder() - self.ui.add_from_file(uifile) + self.ui.add_from_string(_get_data('gourmet', 'ui/preferenceDialog.ui').decode()) self.notebook = self.ui.get_object('notebook') # pref name: {'buttonName':VALUE,...} self.radio_options = radio_options diff --git a/src/gourmet/reccard.py b/src/gourmet/reccard.py index 5cb27b5bb..61bc59447 100644 --- a/src/gourmet/reccard.py +++ b/src/gourmet/reccard.py @@ -4,6 +4,7 @@ import xml.sax.saxutils from gettext import gettext as _ from pathlib import Path +from pkgutil import get_data as _get_data from typing import Any, Callable, Dict, List, Optional, Tuple from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk, Pango @@ -16,7 +17,7 @@ from gourmet.exporters.printer import PrintManager from gourmet.gdebug import debug from gourmet.gglobals import (FLOAT_REC_ATTRS, INT_REC_ATTRS, REC_ATTR_DIC, - REC_ATTRS, uibase) + REC_ATTRS) from gourmet.gtk_extras import WidgetSaver # noqa: imports needed for glade from gourmet.gtk_extras import cb_extras as cb from gourmet.gtk_extras import dialog_extras as de @@ -267,7 +268,7 @@ def setup_actions (self): def setup_ui (self): self.ui = Gtk.Builder() - self.ui.add_from_file(os.path.join(uibase,'recCardDisplay.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/recCardDisplay.ui').decode()) self.ui.connect_signals({ 'shop_for_recipe':self.shop_for_recipe_cb, @@ -1113,7 +1114,7 @@ def setup (self): def setup_main_interface (self): self.ui = Gtk.Builder() - self.ui.add_from_file(os.path.join(uibase,'recCardIngredientsEditor.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/recCardIngredientsEditor.ui').decode()) self.main = self.ui.get_object('ingredientsNotebook') self.main.unparent() self.ingtree_ui = IngredientTreeUI(self, self.ui.get_object('ingTree')) @@ -1346,8 +1347,7 @@ def __init__(self, editor: RecEditor): def setup_main_interface (self): self.ui = Gtk.Builder() - self.ui.add_from_file(os.path.join(uibase, - 'recCardDescriptionEditor.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/recCardDescriptionEditor.ui').decode()) self.imageBox = ImageBox(self) self.init_recipe_widgets() self.ui.connect_signals({ @@ -2975,7 +2975,7 @@ class RecSelector (RecIndex): def __init__(self, recGui, ingEditor): self.prefs = prefs.Prefs.instance() self.ui=Gtk.Builder() - self.ui.add_from_file(os.path.join(uibase,'recipe_index.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/recipe_index.ui').decode()) self.rg=recGui self.ingEditor = ingEditor self.re = self.ingEditor.re diff --git a/src/gourmet/settings.py b/src/gourmet/settings.py index 054da300f..c35c5b2b7 100644 --- a/src/gourmet/settings.py +++ b/src/gourmet/settings.py @@ -7,10 +7,6 @@ base_dir = op.abspath(op.join(op.dirname(__file__), '..')) lib_dir = op.join(base_dir, 'gourmet') -flatpak_ui = "/app/share/gourmet/ui" -ui_base = op.join(op.dirname(__file__), 'ui') -ui_base = flatpak_ui if op.exists(flatpak_ui) else ui_base - locale_base = op.join(base_dir, 'build', 'mo') plugin_base = op.join(base_dir, 'build', 'share', 'gourmet') diff --git a/src/gourmet/shopEditor.py b/src/gourmet/shopEditor.py index 2d5e0a95b..d8e730dee 100644 --- a/src/gourmet/shopEditor.py +++ b/src/gourmet/shopEditor.py @@ -1,11 +1,10 @@ -import os import pickle +from pkgutil import get_data as _get_data import re from gi.repository import GObject, Gtk from .backends import db -from .gglobals import uibase from .gtk_extras import WidgetSaver from .gtk_extras import cb_extras as cb from .gtk_extras import dialog_extras as de @@ -20,7 +19,7 @@ class ShopEditor: def __init__ (self, rd=db.recipeManager(), rg=None): self.ui = Gtk.Builder() - self.ui.add_from_file(os.path.join(uibase,'shopCatEditor.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/shopCatEditor.ui').decode()) self.rd = rd self.rg = rg self.prefs = self.rg.prefs diff --git a/src/gourmet/timer.py b/src/gourmet/timer.py index c2042887e..c11a736fb 100644 --- a/src/gourmet/timer.py +++ b/src/gourmet/timer.py @@ -1,5 +1,4 @@ import os as _os -import os.path import time import xml.sax.saxutils from gettext import gettext as _ @@ -9,7 +8,6 @@ from gi.repository import GLib, Gtk -from gourmet import gglobals from gourmet.gtk_extras import cb_extras as cb from gourmet.gtk_extras.dialog_extras import UserCancelledError, getBoolean from gourmet.sound import Player @@ -150,7 +148,7 @@ class TimerDialog: def __init__ (self): self.init_player() self.ui = Gtk.Builder() - self.ui.add_from_file(os.path.join(gglobals.uibase,'timerDialog.ui')) + self.ui.add_from_string(_get_data('gourmet', 'ui/timerDialog.ui').decode()) self.timer = TimeSpinnerUI( self.ui.get_object('hoursSpinButton'), self.ui.get_object('minutesSpinButton'), diff --git a/src/gourmet/ui/generic_importer.ui b/src/gourmet/ui/generic_importer.ui deleted file mode 100644 index ae6c325be..000000000 --- a/src/gourmet/ui/generic_importer.ui +++ /dev/null @@ -1,391 +0,0 @@ - - - - - - GTK_WINDOW_TOPLEVEL - GTK_WIN_POS_NONE - False - 600 - 600 - True - False - True - False - False - GDK_WINDOW_TYPE_HINT_NORMAL - GDK_GRAVITY_NORTH_WEST - True - False - - - False - 0 - - - True - <i>Select text from the raw recipe and categorize it by labelling which part of the recipe it is (choose from the recipe elements on the right).</i> - False - True - GTK_JUSTIFY_LEFT - True - False - 0 - 0.5 - 12 - 6 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - 0 - False - False - - - - - True - 0.5 - 0.5 - 1 - 1 - 12 - 6 - 12 - 12 - - - True - 2 - 2 - False - 6 - 12 - - - True - _Raw recipe: - True - False - GTK_JUSTIFY_LEFT - False - False - 0 - 0.5 - 0 - 0 - textview - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - 0 - 1 - 0 - 1 - fill - fill - - - - - True - _Label - True - False - GTK_JUSTIFY_LEFT - False - False - 0 - 0.5 - 0 - 0 - treeview1 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - 1 - 2 - 0 - 1 - fill - - - - - - False - 6 - - - True - GTK_POLICY_NEVER - GTK_POLICY_AUTOMATIC - GTK_SHADOW_NONE - GTK_CORNER_TOP_LEFT - - - True - False - False - False - True - False - False - False - - - - - - 0 - True - True - - - - - True - False - 0 - - - True - True - gtk-apply - True - GTK_RELIEF_NORMAL - True - - - - 0 - False - False - GTK_PACK_END - - - - - 0 - False - False - - - - - 1 - 2 - 1 - 2 - fill - fill - - - - - True - False - 6 - - - True - True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_NONE - GTK_CORNER_TOP_LEFT - - - True - True - False - True - GTK_JUSTIFY_LEFT - GTK_WRAP_WORD - True - 0 - 0 - 0 - 0 - 0 - 0 - - - - - - 0 - True - True - - - - - True - False - 6 - - - True - gtk-go-down - True - GTK_RELIEF_NORMAL - True - - - - 0 - False - False - GTK_PACK_END - - - - - True - gtk-go-up - True - GTK_RELIEF_NORMAL - True - - - - 0 - False - False - GTK_PACK_END - - - - - 0 - False - True - - - - - 0 - 1 - 1 - 2 - - - - - - - 0 - True - True - - - - - 12 - True - GTK_BUTTONBOX_END - 6 - - - True - Start new recipe (use this if the file you're importing contains more than one recipe) - True - True - GTK_RELIEF_NORMAL - True - - - - True - 0.5 - 0.5 - 0 - 0 - 0 - 0 - 0 - 0 - - - True - False - 2 - - - True - gtk-new - 4 - 0.5 - 0.5 - 0 - 0 - - - 0 - False - False - - - - - True - _New Recipe - True - False - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - 0 - False - False - - - - - - - - - - - True - True - True - gtk-ok - True - GTK_RELIEF_NORMAL - True - - - - - - 0 - False - False - - - - - - diff --git a/src/gourmet/plugins/nutritional_information/nutritionDruid.ui b/src/gourmet/ui/nutritionDruid.ui similarity index 100% rename from src/gourmet/plugins/nutritional_information/nutritionDruid.ui rename to src/gourmet/ui/nutritionDruid.ui From 79cee7917c2ade772abba108eda4099c9d9dba96 Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Wed, 4 Nov 2020 14:51:32 -0600 Subject: [PATCH 08/12] Remove obsolete plugin search paths All package data is now maintained within the gourmet package itself, so no need to look for plugins in a build directory. --- src/gourmet/gglobals.py | 2 -- src/gourmet/plugin_loader.py | 13 +++++++------ src/gourmet/settings.py | 1 - 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/gourmet/gglobals.py b/src/gourmet/gglobals.py index 8eb560b12..7c078ae24 100644 --- a/src/gourmet/gglobals.py +++ b/src/gourmet/gglobals.py @@ -26,8 +26,6 @@ # Uncomment the below to test FauxThreads # use_threads = False -# note: this stuff must be kept in sync with changes in setup.py -plugin_base = settings.plugin_base REC_ATTRS = [('title', _('Title'), 'Entry'), ('category', _('Category'), 'Combo'), diff --git a/src/gourmet/plugin_loader.py b/src/gourmet/plugin_loader.py index ba3afc1f2..6f99b9b0e 100644 --- a/src/gourmet/plugin_loader.py +++ b/src/gourmet/plugin_loader.py @@ -58,12 +58,13 @@ def instance(cls): return MasterLoader.__single def __init__(self): - self.plugin_directories = [os.path.join(gglobals.gourmetdir,'plugins'), # user plug-ins - os.path.join(current_path,'plugins'), # pre-installed plugins - os.path.join(current_path,'plugins','import_export'), # pre-installed exporter plugins - os.path.join(gglobals.plugin_base,'plugins'), # system-wide plug-ins (required for running from source) - os.path.join(gglobals.plugin_base,'plugins','import_export'), # exporter plug-ins (required for running from source) - ] + self.plugin_directories = [ + # user plug-ins + os.path.join(gglobals.gourmetdir,'plugins'), + # bundled plugins + os.path.join(current_path,'plugins'), + os.path.join(current_path,'plugins','import_export'), + ] self.errors = {} self.pluggables_by_class = {} self.load_plugin_directories() diff --git a/src/gourmet/settings.py b/src/gourmet/settings.py index c35c5b2b7..4ad0539f6 100644 --- a/src/gourmet/settings.py +++ b/src/gourmet/settings.py @@ -8,7 +8,6 @@ lib_dir = op.join(base_dir, 'gourmet') locale_base = op.join(base_dir, 'build', 'mo') -plugin_base = op.join(base_dir, 'build', 'share', 'gourmet') # Apologies for the formatting -- something in the build process is # getting rid of indentations in this file which throws a syntax error From 005d4a8e333ef0732b019f411b4acb23311d8056 Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Wed, 4 Nov 2020 15:45:36 -0600 Subject: [PATCH 09/12] Cleanup setting and use of gourmetdir --- src/gourmet/gglobals.py | 15 ++++++++++----- .../cooksillustrated_plugin.py | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/gourmet/gglobals.py b/src/gourmet/gglobals.py index 7c078ae24..f4ec54426 100644 --- a/src/gourmet/gglobals.py +++ b/src/gourmet/gglobals.py @@ -12,14 +12,19 @@ lib_dir = os.path.join(settings.lib_dir) -gourmetdir: Path = Path(os.environ['HOME']).absolute() / '.gourmet' -if os.name == 'nt': - gourmetdir = Path(os.environ['APPDATA']).absolute() / 'gourmet' - +# TODO: remove the gourmetdir global variable +# Instead of making this a global, it should be passed as an argument to +# interested parties. +# TODO: use standard platform directories to store user-specific data +# On linux, the "~/.gourmet" directory should go into the appropriate XDG user +# directory (or directories). This should also be audited on other platforms. if args.gourmetdir: gourmetdir = Path(args.gourmetdir).absolute() print(f'User specified gourmetdir {gourmetdir}') - +elif os.name == 'nt': + gourmetdir = Path(os.environ['APPDATA']).absolute() / 'gourmet' +else: + gourmetdir = Path(os.environ['HOME']).absolute() / '.gourmet' gourmetdir.mkdir(exist_ok=True) use_threads = args.threads diff --git a/src/gourmet/plugins/import_export/website_import_plugins/cooksillustrated_plugin.py b/src/gourmet/plugins/import_export/website_import_plugins/cooksillustrated_plugin.py index 493f40a90..ad5ce7933 100644 --- a/src/gourmet/plugins/import_export/website_import_plugins/cooksillustrated_plugin.py +++ b/src/gourmet/plugins/import_export/website_import_plugins/cooksillustrated_plugin.py @@ -5,13 +5,13 @@ from selenium import webdriver import gourmet.threadManager -from gourmet.gglobals import gourmetdir from gourmet.gtk_extras import dialog_extras as de from gourmet.plugin import ImportManagerPlugin, PluginPlugin from gourmet.prefs import Prefs from .state import WebsiteTestState + global driver if 'driver' not in globals(): driver = None From c194e92ad3ac691618c170c155c4c250a560dfdf Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Wed, 4 Nov 2020 16:00:46 -0600 Subject: [PATCH 10/12] Remove lib_dir global variable --- src/gourmet/gglobals.py | 3 --- src/gourmet/plugin_loader.py | 5 ++++- src/gourmet/settings.py | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/gourmet/gglobals.py b/src/gourmet/gglobals.py index f4ec54426..a32150739 100644 --- a/src/gourmet/gglobals.py +++ b/src/gourmet/gglobals.py @@ -5,13 +5,10 @@ from gi.repository import Gdk, GdkPixbuf, Gtk -from . import settings from .image_utils import load_pixbuf_from_resource as _load_pixbuf_from_resource from .optionparser import args -lib_dir = os.path.join(settings.lib_dir) - # TODO: remove the gourmetdir global variable # Instead of making this a global, it should be passed as an argument to # interested parties. diff --git a/src/gourmet/plugin_loader.py b/src/gourmet/plugin_loader.py index 6f99b9b0e..2b1a90e82 100644 --- a/src/gourmet/plugin_loader.py +++ b/src/gourmet/plugin_loader.py @@ -58,6 +58,9 @@ def instance(cls): return MasterLoader.__single def __init__(self): + # TODO!!! Discover plugins using namespace packages(?) + # If gourmet is running as a built (i.e., non-source) distribution, + # this is probably not going to work with bundled plugins. self.plugin_directories = [ # user plug-ins os.path.join(gglobals.gourmetdir,'plugins'), @@ -224,7 +227,7 @@ def __init__(self, plugin_info_path: str): with open(plugin_info_path, 'r') as fin: self.load_plugin_file_data(fin) self.curdir, plugin_info_file = os.path.split(plugin_info_path) - plugin_modules_dir = os.path.join(gglobals.lib_dir,"plugins") + plugin_modules_dir = os.path.join(os.path.dirname(__file__), 'plugins') self.plugin_modules_dir = plugin_modules_dir self.import_export_modules_dir = os.path.join(plugin_modules_dir, "import_export") diff --git a/src/gourmet/settings.py b/src/gourmet/settings.py index 4ad0539f6..e22fb75e8 100644 --- a/src/gourmet/settings.py +++ b/src/gourmet/settings.py @@ -5,8 +5,6 @@ # point to the actual data files installation paths. base_dir = op.abspath(op.join(op.dirname(__file__), '..')) -lib_dir = op.join(base_dir, 'gourmet') - locale_base = op.join(base_dir, 'build', 'mo') # Apologies for the formatting -- something in the build process is From 0923959c77b791c1a4c1e66fd16cc25a46d94868 Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Wed, 4 Nov 2020 16:04:59 -0600 Subject: [PATCH 11/12] Remove obsolete settings file --- src/gourmet/settings.py | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/gourmet/settings.py diff --git a/src/gourmet/settings.py b/src/gourmet/settings.py deleted file mode 100644 index e22fb75e8..000000000 --- a/src/gourmet/settings.py +++ /dev/null @@ -1,13 +0,0 @@ -import os.path as op -import sys - -# The following lines are modified at installation time by setup.py so they -# point to the actual data files installation paths. - -base_dir = op.abspath(op.join(op.dirname(__file__), '..')) -locale_base = op.join(base_dir, 'build', 'mo') - -# Apologies for the formatting -- something in the build process is -# getting rid of indentations in this file which throws a syntax error -# on install -if getattr(sys, 'frozen', False): base_dir = op.dirname(sys.executable); data_dir = base_dir; ui_base = op.join(base_dir, 'ui'); doc_base = op.join(base_dir, 'doc'); locale_base = op.join(base_dir, 'locale'); plugin_base = op.join(base_dir) From e64112fec0b34b66fead085303341022709e7c91 Mon Sep 17 00:00:00 2001 From: Karl Nack Date: Thu, 5 Nov 2020 11:44:42 -0600 Subject: [PATCH 12/12] Use '.opus' as the audio file extension --- src/gourmet/timer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gourmet/timer.py b/src/gourmet/timer.py index c11a736fb..b8e058196 100644 --- a/src/gourmet/timer.py +++ b/src/gourmet/timer.py @@ -196,7 +196,7 @@ def play_tune (self) -> None: # TODO!!! Delete the tempfile when we're done # TODO: Figure out how to make GStreamer play raw bytes - fd, fname = _mkstemp('.ogg') + fd, fname = _mkstemp('.opus') _os.write(fd, data) _os.close(fd) self.player.play_file(fname)