From c072366a8c1636e41d76631f0934726f39acb49b Mon Sep 17 00:00:00 2001 From: gnarlyquack Date: Sun, 18 Oct 2020 04:47:19 -0500 Subject: [PATCH 01/24] Ensure recipe index page numbers are integers --- gourmet/gtk_extras/pageable_store.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gourmet/gtk_extras/pageable_store.py b/gourmet/gtk_extras/pageable_store.py index 5852a831c..59f94f2fd 100644 --- a/gourmet/gtk_extras/pageable_store.py +++ b/gourmet/gtk_extras/pageable_store.py @@ -110,14 +110,14 @@ def get_last_page (self): """Return the number of our last page.""" nrecs = int(self._get_length_()) self.per_page = int(self.per_page)#just in case - pages = (nrecs / self.per_page) - 1 + pages = (nrecs // self.per_page) - 1 if nrecs % self.per_page: pages+=1 return pages def change_items_per_page (self, n): current_indx = self.per_page * self.page self.per_page = n - new_page = current_indx / self.per_page + new_page = current_indx // self.per_page self.page = new_page self.update_tree() self.emit('page-changed') @@ -137,7 +137,8 @@ def update_iter (self, itr): indx = path[0] + (self.page * self.per_page) # set takes column number, column value, column number, column value, etc. args = [] - for num_and_col in enumerate(self._get_item_(indx)): args.extend(num_and_col) + for num_and_col in enumerate(self._get_item_(indx)): + args.extend(num_and_col) self.set(itr,*args) From 246dceb1d6ff0b2ee9f332667cac326db6dee32c Mon Sep 17 00:00:00 2001 From: gnarlyquack Date: Sun, 18 Oct 2020 09:04:17 -0500 Subject: [PATCH 02/24] Use correct direction constant in cb_extras (#261) Also change direction from LEFT to RIGHT, otherwise we can't tab past the combobox. --- gourmet/gtk_extras/cb_extras.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gourmet/gtk_extras/cb_extras.py b/gourmet/gtk_extras/cb_extras.py index 17cb41f2c..8985955a3 100644 --- a/gourmet/gtk_extras/cb_extras.py +++ b/gourmet/gtk_extras/cb_extras.py @@ -19,7 +19,8 @@ def focus_out_cb (self, widget, event): parent = widget.get_parent() while parent and not isinstance(parent,Gtk.Window) : parent = parent.get_parent() - for n in range(2): parent.emit('move-focus',Gtk.DIRECTION_LEFT) + for n in range(2): + parent.emit('move-focus',Gtk.DirectionType.RIGHT) #parent.emit('move-focus',Gtk.DIRECTION_LEFT) def keypress_event_cb (self, w, event): From e018eb0b66236e79621f64dd1a70d6e84bd98b27 Mon Sep 17 00:00:00 2001 From: Enno Hermann Date: Tue, 20 Oct 2020 12:28:47 +0200 Subject: [PATCH 03/24] Fix PDF export for recipes with half-stars --- gourmet/plugins/import_export/pdf_plugin/pdf_exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gourmet/plugins/import_export/pdf_plugin/pdf_exporter.py b/gourmet/plugins/import_export/pdf_plugin/pdf_exporter.py index 641e1d5f3..3e336bd8f 100644 --- a/gourmet/plugins/import_export/pdf_plugin/pdf_exporter.py +++ b/gourmet/plugins/import_export/pdf_plugin/pdf_exporter.py @@ -95,7 +95,7 @@ def draw_half_star (self, inner_length=1*inch, outer_length=2*inch, points=5, or inner = False # Start on top is_origin = True #print 'Drawing star with radius',outer_length,'(moving origin ',origin,')' - for theta in range(0,360,360/(points*2)): + for theta in range(0, 360, 360 // (points * 2)): if 0 < theta < 180: continue if inner: r = inner_length else: r = outer_length From 59e78e5655f4923a5facc1adc235d681f9f34bcb Mon Sep 17 00:00:00 2001 From: Enno Hermann Date: Tue, 20 Oct 2020 12:46:32 +0200 Subject: [PATCH 04/24] Fix PDF export for recipes with >4 ingredients --- gourmet/plugins/import_export/pdf_plugin/pdf_exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gourmet/plugins/import_export/pdf_plugin/pdf_exporter.py b/gourmet/plugins/import_export/pdf_plugin/pdf_exporter.py index 3e336bd8f..c07b930bb 100644 --- a/gourmet/plugins/import_export/pdf_plugin/pdf_exporter.py +++ b/gourmet/plugins/import_export/pdf_plugin/pdf_exporter.py @@ -607,7 +607,7 @@ def write_ingfoot (self): # condbreak and a head... ings = self.txt[2:] if len(ings) > 4: - half = (len(ings) / 2) + half = len(ings) // 2 first_half = ings[:-half] second_half = ings[-half:] t = platypus.Table( From 101d6bbf098dab1289fda123ca35c24ce5b861f0 Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Tue, 20 Oct 2020 21:00:15 +0200 Subject: [PATCH 05/24] Remove obsolete flag from build documentation --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1de3bf3c..591044272 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Before you start development, you should first build localized *.mo and *.gourmet-plugin files within a build/ subdirectory of the source tree by running - python3 setup.py build_i18n -m + python3 setup.py build_i18n You can then install Gourmet in edit mode so: From 4ee47e7cdcd935b50d684b12b8d788c24df6ae13 Mon Sep 17 00:00:00 2001 From: Enno Hermann Date: Wed, 21 Oct 2020 22:52:02 +0200 Subject: [PATCH 06/24] Fix unit converter GUI append_text method is deprecated for GtkComboBox, switch to GtkComboBoxText --- gourmet/plugins/unit_converter/converter.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gourmet/plugins/unit_converter/converter.ui b/gourmet/plugins/unit_converter/converter.ui index f84184736..86523f586 100644 --- a/gourmet/plugins/unit_converter/converter.ui +++ b/gourmet/plugins/unit_converter/converter.ui @@ -303,7 +303,7 @@ - + True model1 From bc6de0e75d4ab14fe8f8a76d70c58f431446a36f Mon Sep 17 00:00:00 2001 From: Enno Hermann Date: Wed, 21 Oct 2020 22:57:43 +0200 Subject: [PATCH 07/24] Add dogtail test for unit converter plugin --- gourmet/tests/dogtail/test_unit_converter.py | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 gourmet/tests/dogtail/test_unit_converter.py diff --git a/gourmet/tests/dogtail/test_unit_converter.py b/gourmet/tests/dogtail/test_unit_converter.py new file mode 100644 index 000000000..937e45a32 --- /dev/null +++ b/gourmet/tests/dogtail/test_unit_converter.py @@ -0,0 +1,51 @@ +from dogtail import procedural +from dogtail import tree +from dogtail.utils import run + + +def test_unit_converter(): + """Dogtail integration test: Unit converter plugin behaves as intended.""" + + cmd = "gourmet" + + pid = run(cmd, timeout=3) + gourmet = None + + for app in tree.root.applications(): + if app.get_process_id() == pid: + gourmet = app + break + + assert gourmet is not None, "Could not find Gourmet instance!" + + # Open the unit converter plugin + procedural.keyCombo("T") + procedural.keyCombo("U") + procedural.focus.window("Unit Converter") + + # Enter source amount and unit (5 liters) + procedural.keyCombo("A") + procedural.type("5") + procedural.keyCombo("U") + procedural.keyCombo("") + procedural.click("liter (l)") + + # Enter target unit (ml) + procedural.keyCombo("U") + procedural.keyCombo("") + procedural.keyCombo("Right") + for _ in range(7): + procedural.keyCombo("Down") + procedural.keyCombo("") + + # Check that the result is shown correctly + assert procedural.focus.widget(name="5 l = 5000 ml", roleName="label") + + # There are now two windows, the unit converter, and main window + # Close them successively to quit the application + procedural.keyCombo("") + procedural.keyCombo("") + + +if __name__ == "__main__": + test_unit_converter() From fe744e37f9904579cebe58a71e4c8230f2782fda Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Fri, 23 Oct 2020 22:47:53 +0200 Subject: [PATCH 08/24] PEP gglobals.py --- gourmet/gglobals.py | 102 ++++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/gourmet/gglobals.py b/gourmet/gglobals.py index 9e421692a..8ee2cc811 100644 --- a/gourmet/gglobals.py +++ b/gourmet/gglobals.py @@ -2,12 +2,14 @@ import os.path from pathlib import Path -from gi.repository import Gdk, GdkPixbuf, GObject, Gtk +from gettext import gettext as _ +from gi.repository import Gdk, GdkPixbuf, Gtk -import tempfile from .optionparser import args +from . import settings -tmpdir = tempfile.gettempdir() +uibase = os.path.join(settings.ui_base) +lib_dir = os.path.join(settings.lib_dir) gourmetdir: Path = Path(os.environ['HOME']).absolute() / '.gourmet' if os.name == 'nt': @@ -21,19 +23,14 @@ use_threads = args.threads # Uncomment the below to test FauxThreads -#use_threads = False +# use_threads = False # note: this stuff must be kept in sync with changes in setup.py -from . import settings -uibase = os.path.join(settings.ui_base) -lib_dir = os.path.join(settings.lib_dir) -from gettext import gettext as _ - data_dir = settings.data_dir -imagedir = os.path.join(settings.data_dir,'images') -style_dir = os.path.join(settings.data_dir,'style') +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") +icondir = os.path.join(settings.icon_base, '48x48', 'apps') doc_base = settings.doc_base plugin_base = settings.plugin_base @@ -41,42 +38,42 @@ if args.html_plugin_dir: html_plugin_dir = args.html_plugin_dir else: - html_plugin_dir = os.path.join(gourmetdir,'html_plugins') + 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') + 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') + os.path.join(html_plugin_dir, 'RULES_TEMPLATE') ) -REC_ATTRS = [('title',_('Title'),'Entry'), - ('category',_('Category'),'Combo'), - ('cuisine',_('Cuisine'),'Combo'), - ('rating',_('Rating'),'Entry'), - ('source',_('Source'),'Combo'), - ('link',_('Website'),'Entry'), - ('yields',_('Yield'),'Entry'), - ('yield_unit',_('Yield Unit'),'Combo'), - ('preptime',_('Preparation Time'),'Entry'), - ('cooktime',_('Cooking Time'),'Entry'), +REC_ATTRS = [('title', _('Title'), 'Entry'), + ('category', _('Category'), 'Combo'), + ('cuisine', _('Cuisine'), 'Combo'), + ('rating', _('Rating'), 'Entry'), + ('source', _('Source'), 'Combo'), + ('link', _('Website'), 'Entry'), + ('yields', _('Yield'), 'Entry'), + ('yield_unit', _('Yield Unit'), 'Combo'), + ('preptime', _('Preparation Time'), 'Entry'), + ('cooktime', _('Cooking Time'), 'Entry'), ] -INT_REC_ATTRS = ['rating','preptime','cooktime'] +INT_REC_ATTRS = ['rating', 'preptime', 'cooktime'] FLOAT_REC_ATTRS = ['yields'] -TEXT_ATTR_DIC = {'instructions':_('Instructions'), - 'modifications':_('Notes'), +TEXT_ATTR_DIC = {'instructions': _('Instructions'), + 'modifications': _('Notes'), } -REC_ATTR_DIC={} -NAME_TO_ATTR = {_('Instructions'):'instructions', - _('Notes'):'modifications', - _('Modifications'):'modifications', +REC_ATTR_DIC = {} +NAME_TO_ATTR = {_('Instructions'): 'instructions', + _('Notes'): 'modifications', + _('Modifications'): 'modifications', } DEFAULT_ATTR_ORDER = ['title', - #'servings', + # 'servings', 'yields', 'cooktime', 'preptime', @@ -88,26 +85,29 @@ ] DEFAULT_TEXT_ATTR_ORDER = ['instructions', - 'modifications',] + 'modifications', + ] + -def build_rec_attr_dic (): +def build_rec_attr_dic(): for attr, name, widget in REC_ATTRS: - REC_ATTR_DIC[attr]=name - NAME_TO_ATTR[name]=attr + REC_ATTR_DIC[attr] = name + NAME_TO_ATTR[name] = attr + build_rec_attr_dic() DEFAULT_HIDDEN_COLUMNS = [REC_ATTR_DIC[attr] for attr in - ['link','yields','yield_unit','preptime','cooktime'] - ] + ('link', 'yields', 'yield_unit', 'preptime', 'cooktime')] # noqa # Set up custom STOCK items and ICONS! icon_factory = Gtk.IconFactory() -def add_icon (file_name, stock_id, label=None, modifier=0, keyval=0): + +def add_icon(file_name, stock_id, label=None, modifier=0, keyval=0): pb = GdkPixbuf.Pixbuf.new_from_file(file_name) iconset = Gtk.IconSet.new_from_pixbuf(pb) - icon_factory.add(stock_id,iconset) + icon_factory.add(stock_id, iconset) icon_factory.add_default() # TODO: fix adding icons return @@ -117,9 +117,17 @@ def add_icon (file_name, stock_id, label=None, modifier=0, keyval=0): keyval, "")]) -for filename,stock_id,label,modifier,keyval in [ - ('AddToShoppingList.png','add-to-shopping-list',_('Add to _Shopping List'),Gdk.ModifierType.CONTROL_MASK,Gdk.keyval_from_name('l')), - ('reccard.png','recipe-card',None,0,0), - ('reccard_edit.png','edit-recipe-card',None,0,0), - ]: - add_icon(os.path.join(imagedir,filename),stock_id,label,modifier,keyval) + +for filename, stock_id, label, modifier, keyval in [ + ('AddToShoppingList.png', + 'add-to-shopping-list', + _('Add to _Shopping List'), + Gdk.ModifierType.CONTROL_MASK, + Gdk.keyval_from_name('l')), + + ('reccard.png', 'recipe-card', None, 0, 0), + + ('reccard_edit.png', 'edit-recipe-card', None, 0, 0), + ]: + add_icon(os.path.join(imagedir, filename), stock_id, + label, modifier, keyval) From 27f2893324bab02ecb743aae2b32911ab285d7ae Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Fri, 23 Oct 2020 22:49:24 +0200 Subject: [PATCH 09/24] Set link colours depending on theme brightness Lighter themes have blue, darker themes get deeppink --- gourmet/gglobals.py | 13 +++++++++++++ gourmet/gtk_extras/LinkedTextView.py | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/gourmet/gglobals.py b/gourmet/gglobals.py index 8ee2cc811..972cf15dd 100644 --- a/gourmet/gglobals.py +++ b/gourmet/gglobals.py @@ -131,3 +131,16 @@ def add_icon(file_name, stock_id, label=None, modifier=0, keyval=0): ]: add_icon(os.path.join(imagedir, filename), stock_id, label, modifier, keyval) + + +# Color scheme preference +LINK_COLOR = 'blue' +STARS = 'blue' + +style = Gtk.StyleContext.new() +_, bg_color = style.lookup_color('bg_color') +_, fg_color = style.lookup_color('fg_color') + +if sum(fg_color) > sum(bg_color): # background is darker + LINK_COLOR = 'deeppink' + STARS = 'gold' diff --git a/gourmet/gtk_extras/LinkedTextView.py b/gourmet/gtk_extras/LinkedTextView.py index b8497c8be..abcfed490 100644 --- a/gourmet/gtk_extras/LinkedTextView.py +++ b/gourmet/gtk_extras/LinkedTextView.py @@ -22,6 +22,7 @@ import re from typing import Optional from gi.repository import Gdk, GObject, Gtk, Pango +from gourmet.gglobals import LINK_COLOR from gourmet.gtk_extras.pango_buffer import PangoBuffer from gourmet.gtk_extras.pango_html import PangoToHtml @@ -29,9 +30,9 @@ class LinkedPangoBuffer(PangoBuffer): href_regexp = re.compile(r"]*>(.*?)") - url_markup = 'underline="single" color="blue"' + url_markup = f'underline="single" color="{LINK_COLOR}"' url_props = [('underline', Pango.Underline.SINGLE), - ('foreground-gdk', Gdk.color_parse('blue'))] + ('foreground-gdk', LINK_COLOR)] markup_dict = {} def set_text(self, txt: str) -> None: From c61460115256b0fef74b607233cd5bd1c2739210 Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Sat, 24 Oct 2020 00:24:07 +0200 Subject: [PATCH 10/24] Select star files based on theme lightness --- gourmet/data/images/blue_star.png | Bin 0 -> 565 bytes gourmet/data/images/half_blue_star.png | Bin 0 -> 689 bytes gourmet/gglobals.py | 8 +++- gourmet/gtk_extras/ratingWidget.py | 49 ++++++++++++------------- setup.py | 7 +--- 5 files changed, 32 insertions(+), 32 deletions(-) create mode 100644 gourmet/data/images/blue_star.png create mode 100644 gourmet/data/images/half_blue_star.png diff --git a/gourmet/data/images/blue_star.png b/gourmet/data/images/blue_star.png new file mode 100644 index 0000000000000000000000000000000000000000..986f7785b6fcb39cc98b388df8a7e8f7149092aa GIT binary patch literal 565 zcmV-50?Pe~P)f`)EIa8MU16a)uxshb5yp-|i$97S-t_kAykLqHr#LLIdrE-Hn9xCr;% zdp={@qD#R+s<`;w_wt%hqk?hJBI!AV)}S^oqzAVgcR&7j|I1{Ch#oU-kcfs?>S9{0a9sa41Un}CLfUezl!(TeI{y>~UxgP@!HhP@lQ4Zc%UXbQqf5|Npxn ze}PH-KqRgbz#AmM0TzBd{7;B6CSi<8aDSza0zM<443W5Md+KI7SLo(4>YZFLwZ@p5 zvXd~zB-VdXJY@&^Dg~Y4LQOD%PZ;2HBJ~Ub^{E7MPmj2MA(@f-tOL;y3vkf!^8Fc5 zeKaVZVq&q=2~~?lL1zdkO+c9~kHj?w@ogtmEi|iJN4;XZJsWk{vr)&WS8R7>oZ`Md zPXZjUri|G`M0|xw{k9TFgt}7#X;!zoej(}lg`_Lvl$|Y}qh5S$%9vfH+-fqQ`iR8W zMB>j)L5S{^z!JuN>~Xw&zw=Tb+H@dp4v!P5XNXka%RvVC#TL>1axep`kNem&9;*6^ zQ?|u&69as+5#+um<)8!65cgN=s;Z^-d0 z_m$1XQgbQ4bgt0N{r4xUf>f%zgnrTM!mMgHrFpC3FNy7oabX8)LsPLKcx(z!ynE8`TGQSZ}G?b{N3AobDT1Hn8OYJ!WU z=0CetiewHOm{{y|ME;B; sum(bg_color): # background is darker LINK_COLOR = 'deeppink' - STARS = 'gold' + star_color = 'gold' + +NO_STAR = Path(__file__).parent / 'data' / 'images' / 'no_star.png' +HALF_STAR = Path(__file__).parent / 'data' / 'images' / f'half_{star_color}_star.png' # noqa +FULL_STAR = Path(__file__).parent / 'data' / 'images' / f'{star_color}_star.png' # noqa diff --git a/gourmet/gtk_extras/ratingWidget.py b/gourmet/gtk_extras/ratingWidget.py index f3dfd178e..af1068eaf 100644 --- a/gourmet/gtk_extras/ratingWidget.py +++ b/gourmet/gtk_extras/ratingWidget.py @@ -1,13 +1,12 @@ -from gi.repository import Gdk, GdkPixbuf, GObject, Gtk -import gourmet.gglobals as gglobals import os.path -from gettext import gettext as _ import tempfile -try: - from PIL import Image -except ImportError: - import Image +from gettext import gettext as _ +from gi.repository import Gdk, GdkPixbuf, GObject, Gtk +from PIL import Image + +import gourmet.gglobals as gglobals + PLUS_ONE_KEYS = ['plus', 'greater', @@ -37,24 +36,24 @@ class StarGenerator: set_image and unset_image must have the same width!""" - def __init__ (self, - set_image=os.path.join(gglobals.imagedir,'gold_star.png'), - unset_image=os.path.join(gglobals.imagedir,'no_star.png'), - half_image=os.path.join(gglobals.imagedir,'half_gold_star.png'), - #background=(0,0,0,0), - background=0, - size=None, - ): - self.set_img = Image.open(set_image) - if size: - self.set_img = self.set_img.resize(size) - self.unset_img = Image.open(unset_image).resize(self.set_img.size) - self.halfset_img = Image.open(half_image).resize(self.set_img.size) - #convert to RGBA self.set_img = self.set_img.convert('RGBA') - self.set_img = self.set_img.convert('RGBA') - self.unset_img = self.unset_img.convert('RGBA') - self.halfset_img = self.halfset_img.convert('RGBA') - self.width,self.height = self.set_img.size + def __init__(self, + set_image=gglobals.FULL_STAR, + unset_image=gglobals.NO_STAR, + half_image=gglobals.HALF_STAR, + background=0, + size=None): + set_img = Image.open(set_image) + + if size is not None: + set_img = self.set_img.resize(size) + + unset_img = Image.open(unset_image).resize(set_img.size) + halfset_img = Image.open(half_image).resize(set_img.size) + + self.set_img = set_img.convert('RGBA') + self.unset_img = unset_img.convert('RGBA') + self.halfset_img = halfset_img.convert('RGBA') + self.width, self.height = self.set_img.size self.set_region = self.set_img.crop((0,0, self.width, self.height)) diff --git a/setup.py b/setup.py index 0f4d8abbb..a3162323b 100644 --- a/setup.py +++ b/setup.py @@ -98,13 +98,8 @@ def crawl_plugins(base, basename): package_data = [ 'backends/default.db', - 'plugins/*/*.ui', - 'plugins/*/images/*.png', - 'plugins/*/*/images/*.png', 'plugins/*.gourmet-plugin', 'plugins/*/*.gourmet-plugin', - 'ui/*.ui', - 'ui/catalog/*', 'data/recipe.dtd', 'data/WEIGHT.txt', 'data/FOOD_DES.txt', @@ -114,7 +109,9 @@ def crawl_plugins(base, basename): 'data/images/reccard_edit.png', 'data/images/AddToShoppingList.png', 'data/images/half_gold_star.png', + 'data/images/half_blue_star.png', 'data/images/gold_star.png', + 'data/images/blue_star.png', 'data/images/reccard.png', 'data/sound/phone.wav', 'data/sound/warning.wav', From bc15dbfc98d7ec027c7145d0bd2bd78b6e44c6c9 Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Sat, 24 Oct 2020 02:15:22 +0200 Subject: [PATCH 11/24] Fix MyCookBook/CookMate import/export --- .../mycookbook_plugin/mycookbook_exporter.py | 9 +++++---- .../mycookbook_plugin/mycookbook_importer.py | 2 +- .../mycookbook_plugin/mycookbook_importer_plugin.py | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_exporter.py b/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_exporter.py index 86406e19a..fc24a2208 100644 --- a/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_exporter.py +++ b/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_exporter.py @@ -66,15 +66,16 @@ def write_text (self, attr, text): self.attrlist_el.appendChild(attr_el) - def write_image (self, image): + def write_image(self, image: bytes): # write image file to the temp directory - imageFilename = unicodedata.normalize('NFKD', str(self.current_title + '.png')).encode('ascii', 'ignore') - pic_fullpath = os.path.join(tempfile.gettempdir(),'images',imageFilename) + img_fname = f'{self.current_title}.png' + pic_fullpath = os.path.join(tempfile.gettempdir(),'images', img_fname) result = gourmet.image_utils.bytes_to_image(image) result.save(pic_fullpath) # write imagepath in the xml - self.rec_el.appendChild(self.create_text_element('imagepath','images/' + imageFilename)) + self.rec_el.appendChild(self.create_text_element('imagepath', + f'images/{img_fname}')) def handle_italic (self, chunk): return chunk def handle_bold (self, chunk): return chunk diff --git a/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_importer.py b/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_importer.py index d82b047ce..9adc23d40 100644 --- a/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_importer.py +++ b/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_importer.py @@ -55,7 +55,7 @@ def endElement (self, name): if not hasattr(self,'db'): import gourmet.backends.db as db self.db = db.get_database() - ingdic = self.db.parse_ingredient(self.elbuf.strip()) + ingdic = self.rd.parse_ingredient(self.elbuf.strip()) self.start_ing(**ingdic) self.commit_ing() if name=='li' and self.current_section=='instruction': diff --git a/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_importer_plugin.py b/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_importer_plugin.py index cddce5f88..b2b51ad1d 100644 --- a/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_importer_plugin.py +++ b/gourmet/plugins/import_export/mycookbook_plugin/mycookbook_importer_plugin.py @@ -43,9 +43,9 @@ def get_importer (self, filename): parser = etree.XMLParser(recover=True) tree = etree.parse(xmlfilename, parser) fixedxmlfilename = xmlfilename+'fixed' - outFile = open(fixedxmlfilename, 'w') - tree.write(outFile, xml_declaration=True, encoding='utf-8', pretty_print=True) - outFile.close() + with open(fixedxmlfilename, 'wb') as fout: + tree.write(fout, xml_declaration=True, + encoding='utf-8', pretty_print=True) zf.close() From 469fc1ecfc4a1403af2879c740500b8df9a6f3ea Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Sat, 24 Oct 2020 13:05:57 +0200 Subject: [PATCH 12/24] Use a single config file for all preferences Active plugins are now saved with the other preferences via prefs.Prefs. The guiprefs.toml file is now deprecated in favour of preferences.toml --- gourmet/plugin_gui.py | 37 +++++++++--------- gourmet/plugin_loader.py | 81 ++++++++++++++++------------------------ gourmet/prefs.py | 2 +- gourmet/tests/test_db.py | 5 ++- 4 files changed, 56 insertions(+), 69 deletions(-) diff --git a/gourmet/plugin_gui.py b/gourmet/plugin_gui.py index abca72ca1..b035cdd77 100644 --- a/gourmet/plugin_gui.py +++ b/gourmet/plugin_gui.py @@ -1,18 +1,20 @@ -from . import plugin_loader -from gi.repository import GObject, Gtk -from .gtk_extras import dialog_extras as de +from typing import Any, List, Tuple from xml.sax.saxutils import escape + from gettext import gettext as _ -from typing import Any, Optional +from gi.repository import GObject, Gtk + +from .plugin_loader import DependencyError, MasterLoader +from .gtk_extras import dialog_extras as de + class PluginChooser: def __init__ (self): - self.loader = plugin_loader.get_master_loader() + self.loader = MasterLoader.instance() self.window = Gtk.Dialog() self.notebook = Gtk.Notebook() for cat,plugins in list(self.categorize_plugins().items()): - #self.make_treeview(self.loader.active_plugin_sets) plugin_view = self.make_treeview(plugins) lab = Gtk.Label(label=cat); lab.show() self.notebook.append_page(plugin_view,lab) @@ -50,20 +52,19 @@ def categorize_plugins (self): categorized[cat].append((module_name,plugin_set)) return categorized - def make_list_store (self, plugin_list): - ls = Gtk.ListStore(bool, # activated - GObject.TYPE_PYOBJECT, # the plugin-set object with all other info - ) - for module_name,plugin_set in plugin_list: #self.loader.available_plugin_sets.items(): - ls.append( - (module_name in self.loader.active_plugin_sets, - plugin_set) - ) + def make_list_store(self, plugin_list: List[Tuple[str, 'PluginSet']]) -> Gtk.ListStore: + ls = Gtk.ListStore(bool, # plugin activated + GObject.TYPE_PYOBJECT) # plugin and its info + for module_name, plugin_set in plugin_list: + ls.append((module_name in self.loader.active_plugin_sets, plugin_set)) return ls @staticmethod - def plugin_description_formatter(col: Gtk.TreeViewColumn, renderer: Gtk.CellRendererText, - mod: Gtk.ListStore, itr: Gtk.TreeIter, data: Any) -> None: + def plugin_description_formatter(col: Gtk.TreeViewColumn, + renderer: Gtk.CellRendererText, + mod: Gtk.ListStore, + itr: Gtk.TreeIter, + data: Any): """ Format plugin name and description in the plugin window """ plugin_set = mod[itr][1] @@ -107,7 +108,7 @@ def do_change_plugin (self, plugin_set, state, ls): if state: try: self.loader.check_dependencies(plugin_set) - except plugin_loader.DependencyError as dep_error: + except DependencyError as dep_error: print('Missing dependencies:',dep_error.dependencies) for row in ls: ps = row[1] diff --git a/gourmet/plugin_loader.py b/gourmet/plugin_loader.py index 0ab0e5904..10ef67765 100644 --- a/gourmet/plugin_loader.py +++ b/gourmet/plugin_loader.py @@ -1,31 +1,33 @@ -PRE = 0 -POST = 1 +import glob +import os.path +import sys +from typing import List + from gourmet import gglobals -import os.path, glob, sys +from gourmet.prefs import Prefs from . import plugin from .gdebug import debug from .defaults.defaults import loc +PRE = 0 +POST = 1 + try: - current_path = os.path.split(os.path.join(os.getcwd(),__file__))[0] -except: + current_path = os.path.split(os.path.join(os.getcwd(), __file__))[0] +except IndexError: current_path = '' -# This module provides a base class for loading plugins. Everything -# that is plug-in-able in Gourmet should subclass the plugin loader. - -# Everything that is a plugin needs to provide a python module with a -# plugins attribute containing the plugin classes that make up the -# plugin. In addition, we need a .gourmet-plugin configuration file -# pointing to the module (with the module parameter) and giving the -# name and comment for the plugin. class MasterLoader: - - # Singleton design pattern lifted from: - # http://www.python.org/workshops/1997-10/proceedings/savikko.html - # to get an instance, use the convenience function - # MasterLoader.instance() + """This module provides a base class for loading plugins. Everything + that is plug-in-able in Gourmet should subclass the plugin loader. + + Everything that is a plugin needs to provide a python module with a plugins + attribute containing the plugin classes that make up the plugin. + In addition, we need a .gourmet-plugin configuration file pointing to the + module (with the module parameter) and giving the name and comment for the + plugin. + """ __single = None default_active_plugin_sets = [ # tools @@ -46,7 +48,6 @@ class MasterLoader: 'mycookbook_plugin', 'epub_plugin', ] - active_plugin_filename = os.path.join(gglobals.gourmetdir,'active_plugins') @classmethod def instance(cls): @@ -55,7 +56,7 @@ def instance(cls): return MasterLoader.__single - def __init__ (self): + 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 @@ -65,6 +66,7 @@ def __init__ (self): self.errors = {} self.pluggables_by_class = {} self.load_plugin_directories() + self.active_plugin_sets: List[str] = [] self.load_active_plugins() def load_plugin_directories (self): @@ -82,16 +84,14 @@ def load_plugin_directories (self): else: self.available_plugin_sets[plugin_set.module] = plugin_set - def load_active_plugins (self): - """Activate plugins that have been activated on startup - """ - if os.path.exists(self.active_plugin_filename): - infi = open(self.active_plugin_filename,'r') - self.active_plugin_sets = [l.strip() for l in infi.readlines()] - else: - self.active_plugin_sets = self.default_active_plugin_sets[:] + def load_active_plugins(self): + """Enable plugins that were previously saved to the preferences""" + prefs = Prefs.instance() + self.active_plugin_sets = prefs.get('plugins', + self.default_active_plugin_sets[:]) self.active_plugins = [] self.instantiated_plugins = {} + for p in self.active_plugin_sets: if p in self.available_plugin_sets: try: @@ -105,25 +105,12 @@ def load_active_plugins (self): else: print('Plugin ',p,'not found') + def save_active_plugins(self): + prefs = Prefs.instance() + prefs['plugins'] = self.active_plugin_sets + prefs.save() - def save_active_plugins (self): - # If we have not changed from the defaults and no - # configuration file exists, don't bother saving one. - if ((self.active_plugin_sets != self.default_active_plugin_sets) - or - os.path.exists(self.active_plugin_filename)): - ofi = open(self.active_plugin_filename,'w') - saved = [] # keep track of what we've written to avoid - # saving a plugin twice - for plugin_set in self.active_plugin_sets: - if not plugin_set in saved: - ofi.write(plugin_set+'\n') - saved.append(plugin_set) - ofi.close() - #elif self.active_plugin_sets == self.default_active_plugin_sets: - # print 'No change to plugins, nothing to save.' - - def check_dependencies (self, plugin_set): + def check_dependencies(self, plugin_set): if plugin_set.dependencies: missing = [] depends = plugin_set.dependencies or [] @@ -219,8 +206,6 @@ def register_pluggable (self, pluggable, klass): def unregister_pluggable (self, pluggable, klass): self.pluggables_by_class[klass].remove(pluggable) -def get_master_loader (): - return MasterLoader.instance() class PluginSet: """A lazy-loading set of plugins. diff --git a/gourmet/prefs.py b/gourmet/prefs.py index a4c2bd5a7..c3a104d20 100644 --- a/gourmet/prefs.py +++ b/gourmet/prefs.py @@ -21,7 +21,7 @@ def instance(cls): return Prefs.__single - def __init__(self, filename='guiprefs.toml'): + def __init__(self, filename='preferences.toml'): super().__init__() self.filename = Path(gourmetdir) / filename self.set_hooks = [] diff --git a/gourmet/tests/test_db.py b/gourmet/tests/test_db.py index 3af62a469..dd4f709e1 100644 --- a/gourmet/tests/test_db.py +++ b/gourmet/tests/test_db.py @@ -1,12 +1,13 @@ import tempfile, unittest + from gourmet.backends import db +from gourmet.plugin_loader import MasterLoader class DBTest (unittest.TestCase): def setUp (self): print('Calling setUp') # Remove all plugins for testing purposes - from gourmet.plugin_loader import get_master_loader - ml = get_master_loader() + ml = MasterLoader.instance() ml.save_active_plugins = lambda *args: True; # Don't save anything we do to plugins ml.active_plugins = [] ml.active_plugin_sets = [] From 698995b1b7aa6571c49c4da9407d48837c4c78c7 Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Sun, 25 Oct 2020 16:09:46 +0100 Subject: [PATCH 13/24] Clean KeyManager --- gourmet/backends/db.py | 6 +- gourmet/importers/importer.py | 5 +- gourmet/keymanager.py | 253 ++++++++-------------------------- 3 files changed, 65 insertions(+), 199 deletions(-) diff --git a/gourmet/backends/db.py b/gourmet/backends/db.py index ca79df06b..faa6a57a4 100644 --- a/gourmet/backends/db.py +++ b/gourmet/backends/db.py @@ -17,9 +17,9 @@ from gourmet.gdebug import debug, TimeAction import gourmet.gglobals as gglobals -from gourmet import Undo, keymanager, convert +from gourmet import Undo, convert, image_utils from gourmet.defaults import lang as defaults -from gourmet import image_utils +from gourmet.keymanager import KeyManager import gourmet.version import gourmet.recipeIdentifier as recipeIdentifier from gourmet.plugin_loader import Pluggable, pluggable_method @@ -1877,7 +1877,7 @@ def instance_for( def __init__ (self, *args, **kwargs): debug('recipeManager.__init__()',3) self.rd = get_database(*args, **kwargs) - self.km = keymanager.get_keymanager(rm=self) + self.km = KeyManager.instance(recipe_manager=self) def __getattr__(self, name): # RecipeManager was previously a subclass of RecData. diff --git a/gourmet/importers/importer.py b/gourmet/importers/importer.py index 1ab710def..7e51570e4 100644 --- a/gourmet/importers/importer.py +++ b/gourmet/importers/importer.py @@ -6,10 +6,11 @@ from gettext import gettext as _ import xml.sax.saxutils -from gourmet import keymanager, convert, image_utils +from gourmet import convert, image_utils from gourmet.gdebug import debug, TimeAction, print_timer_info import gourmet.gglobals import gourmet.gtk_extras.dialog_extras as de +from gourmet.keymanager import KeyManager from gourmet.recipeManager import get_recipe_manager # Get hold of database from gourmet.threadManager import SuspendableThread, Terminated @@ -90,7 +91,7 @@ def __init__ (self, else: self.rating_converter = RatingConverter() self.do_conversion = True - self.km = keymanager.get_keymanager() + self.km = KeyManager.instance() timeaction.end() SuspendableThread.__init__(self, name=name) diff --git a/gourmet/keymanager.py b/gourmet/keymanager.py index bba244fa4..c9a3796f0 100644 --- a/gourmet/keymanager.py +++ b/gourmet/keymanager.py @@ -1,21 +1,13 @@ from collections import defaultdict import re -import time from typing import List, Optional, Tuple from .defaults.defaults import lang as defaults from .defaults.defaults import langProperties as langProperties -from .gdebug import debug, TimeAction note_separator_regexp = r'(;|\s+-\s+|--)' note_separator_matcher = re.compile(note_separator_regexp) -def snip_notes (s): - m = note_separator_matcher.search(s) - if not m: return s - ret = s[:m.start()].strip() - if ret: return ret - else: return s class KeyManager: @@ -25,121 +17,109 @@ class KeyManager: __single = None @classmethod - def instance(cls, *args, **kwargs): + def instance(cls, recipe_manager=None): if KeyManager.__single is None: - KeyManager.__single = cls(*args, **kwargs) + KeyManager.__single = cls(recipe_manager) return KeyManager.__single - def __init__ (self, kd={}, rm=None): - self.kd = kd - if not rm: - from . import recipeManager - rm = recipeManager.default_rec_manager() - self.rm = rm - self.cooking_verbs=cooking_verbs - # This needs to be made sane i18n-wise - self.ignored = defaults.IGNORE - self.ignored.extend(self.cooking_verbs) - self.ignored_regexp = re.compile("[,; ]?(" + '|'.join(self.ignored) + ")[,; ]?") + def __init__(self, recipe_manager=None): + if recipe_manager is None: + from . import recipeManager # work around cyclic dependencies + recipe_manager = recipeManager.default_rec_manager() + self.rm = recipe_manager + if self.rm.fetch_len(self.rm.keylookup_table) == 0: self.initialize_from_defaults() self.initialize_categories() - def initialize_from_defaults (self): + @staticmethod + def _snip_notes(s: str) -> str: + m = note_separator_matcher.search(s) + if not m: + return s + ret = s[:m.start()].strip() + + return ret if ret else s + + def initialize_from_defaults(self): dics = [] - for key,items in list(defaults.keydic.items()): + for key, items in defaults.keydic.items(): for i in items: - dics.append( - {'ingkey':str(key), - 'item':str(i), - 'count':1} - ) + dics.append({'ingkey': str(key), + 'item': str(i), + 'count': 1}) self.rm.keylookup_table.insert().execute(dics) - def make_regexp_for_strings (self, ignored): - ret = "(" - ignored.join("|") - - def regexp_for_all_words (self, txt): + def regexp_for_all_words(self, txt): """Return a regexp to match any of the words in string.""" - regexp=r"(^|\W)(" - count=0 + regexp = r"(^|\W)(" + count = 0 for w in self.word_splitter.split(txt): - #for each keyword, we create a search term - if w: #no blank strings! + # for each keyword, we create a search term + if w: # no blank strings! count += 1 - regexp="%s%s|"%(regexp,re.escape(w)) - regex=r"%s)(?=\W|$)"%(regexp[0:-1]) #slice off extra | + regexp = "%s%s|" % (regexp, re.escape(w)) + regex = r"%s)(?=\W|$)" % (regexp[0:-1]) # slice off extra | if count: return re.compile(regex), count else: return None, count - def initialize_categories (self): + def initialize_categories(self): """We treat things like flour as categories, usually designated as "flour, all purpose" or "flour, whole wheat". We look for this sort of thing, assuming the noun, descriptor format has been followed previously. With our handy list, we will more easily be able to guess correctly that barley flour should be flour, barley""" - debug("Start initialize_categories",10) self.cats = [] - for k in self.rm.get_unique_values('ingkey',self.rm.ingredients_table,deleted=False): - fnd=k.find(',') + for k in self.rm.get_unique_values('ingkey', + self.rm.ingredients_table, + deleted=False): + fnd = k.find(',') if fnd != -1: self.cats.append(k[0:fnd]) - debug("End initialize_categories",10) - def grab_ordered_key_list (self, str): - debug("Start grab_ordered_key_list",10) + def grab_ordered_key_list(self, str): """We return a list of potential keys for string.""" - kl=self.look_for_key(str) + kl = self.look_for_key(str) gk = self.generate_key(str) nwlst = [] added_gk = False - for key,rnk in kl: + for key, rnk in kl: if rnk < 0.9 and not added_gk: if not nwlst.__contains__(gk): nwlst.append(gk) added_gk = True if not nwlst.__contains__(key): nwlst.append(key) - if not added_gk : + if not added_gk: if not nwlst.__contains__(gk): nwlst.append(gk) - debug("End grab_ordered_key_list",10) return nwlst - def get_key_fast (self, s): - try: - if s: srch = self.rm.fetch_all(self.rm.keylookup_table, - item=s, - sort_by=[('count',1)] - ) - else: srch = None - except: - print('error seeking key for ',s) - raise + def get_key_fast(self, s) -> str: + srch = self.rm.fetch_all(self.rm.keylookup_table, item=s, + sort_by=[('count', 1)]) + if srch: + return srch[-1].ingkey else: - if srch: return srch[-1].ingkey - else: - s = snip_notes(s) - return self.generate_key(s) + s = self._snip_notes(s) + return self.generate_key(s) - def get_key (self,txt, certainty=0.61): + def get_key(self, txt: str, certainty: Optional[float] = 0.61) -> str: """Grab a single key. This is simply a best guess at the right key for an item (we can't be sure -- if we could be, we wouldn't need a key system in the first place!""" - debug("Start get_key %s"%str,10) - if not txt: return '' - txt = snip_notes(txt) + if not txt: + return '' + txt = self._snip_notes(txt) result = self.look_for_key(txt) if result and result[0][0] and result[0][1] > certainty: - k=result[0][0] + k = result[0][0] else: - k=self.generate_key(txt) - debug("End get_key",10) + k = self.generate_key(txt) return k def look_for_key(self, txt: str) -> Optional[List[Tuple[str, float]]]: @@ -213,7 +193,7 @@ def look_for_key(self, txt: str) -> Optional[List[Tuple[str, float]]]: ik = match.ingkey words_in_key = len(ik.split()) wordcount = words_in_key if words_in_key > nwords else nwords - retvals[ik] += (match.count / total_count) * (1.0 / wordcount) + retvals[ik] += (match.count / total_count) * (1 / wordcount) # Add some probability if our word shows up in the key if word in ik: @@ -223,135 +203,20 @@ def look_for_key(self, txt: str) -> Optional[List[Tuple[str, float]]]: retv.sort(key=lambda x: x[1]) return retv - def generate_key(self, ingr): + def generate_key(self, ingr: str) -> str: """Generate a generic-looking key from a string.""" - timer = TimeAction('keymanager.generate_key 1',3) - debug("Start generate_key(self,%s)"%ingr,10) ingr = ingr.strip() - # language specific here - turn off the strip().lower() for German, 'cos: - # i) german Nouns always start with an uppercase Letter. - # ii) the function 'lower()' doesn't appear to work correctly with umlauts. - if (not langProperties['capitalisedNouns']): - # We want to use unicode's lower() method - if not isinstance(ingr,str): - ingr = str(ingr.decode('utf8')) + + if not langProperties['capitalisedNouns']: + # language specific here - turn off the strip().lower() for eg. + # German nouns that always start with an uppercase Letter. ingr = ingr.lower() - timer.end() - timer = TimeAction('keymanager.generate_key 2',3) - debug("verbless string=%s"%ingr,10) + if ingr.find(',') == -1: # if there are no commas, we see if it makes sense # to turn, e.g. whole-wheat bread into bread, whole-wheat words = ingr.split() if len(words) >= 2: if self.cats.__contains__(words[-1]): - ingr = "%s, %s" %(words[-1],''.join(words[0:-1])) - #if len(str) > 32: - # str = str[0:32] - debug("End generate_key",10) - timer.end() + ingr = f'{words[-1]}, {"".join(words[0:-1])}' return ingr - - def sing_equal(self, str1, str2): - debug("Start sing_equal(self,%s,%s)"%(str1,str2),10) - sing_str1 = self.remove_final_s(str1) - sing_str2 = self.remove_final_s(str2) - return sing_str1 == sing_str2 - - def remove_verbs (self,words): - """Handed a list of words, we remove anything from the - list that matches a regexp in self.ignored""" - debug("Start remove_verbs",10) - t=TimeAction('remove_verbs',0) - stringp=True - if isinstance(words, list): - stringp=False - words = " ".join(words) - words = words.split(';')[0] #we ignore everything after semicolon - words = words.split("--")[0] # we ignore everything after double dashes too! - m = self.ignored_regexp.match(words) - while m: - words = words[0:m.start()] + words[m.end():] - m = self.ignored_regexp.match(words) - t.end() - if stringp: - return words - else: - return words.split() - - -class KeyDictionary: - def __init__ (self, rm): - """We create a readonly dictionary based on the metakit ingredients_table table.""" - self.rm = rm - self.default = defaults.keydic - - def has_key (self, k): - debug('has_key testing for %s'%k,1) - if self.rm.fetch_one(self.rm.ingredients_table,item=k): return True - elif k in self.default: return True - else: return False - - def srt_by_2nd (self, i1, i2): - """Sort by the reverse order of the second item in each of i1 - and i2""" - if i1[1] < i2[1]: - return 1 - if i2[1] < i1[1]: - return -1 - else: return 0 - - def __getitem__ (self, k): - kvw = self.rm.fetch_count( - self.rm.ingredients_table, - 'ingkey', - sort_by=('count',-1), - item=k - ) - - def keys (self): - ll = self.rm.get_unique_values('item',self.rm.ingredients_table,deleted=False) - ll.extend(list(self.default.keys())) - return ll - - def values (self): - ll = self.rm.get_unique_values('ingkey',self.rm.ingredients_table,deleted=False) - ll.extend(list(self.default.values())) - return ll - - def items (self): - lst = [] - for i in list(self.keys()): - lst.append((i, self.__getitem__(i))) - lst.extend(list(self.default.items())) - return lst - -cooking_verbs=["cored", - "peeled", - "sliced", - "chopped", - "diced", - "pureed", - "blended", - "grated", - "minced", - "cored", - "heated", - "warmed", - "chilled"] - -def get_keymanager (*args, **kwargs): - return KeyManager.instance(*args,**kwargs) - -if __name__ == '__main__': - - def timef (f): - t = time.time() - f() - print(time.time()-t) - import tempfile - from . import recipeManager - km = KeyManager(rm=recipeManager.RecipeManager(**recipeManager.dbargs)) - recipeManager.dbargs['file']=tempfile.mktemp('.mk') - fkm = KeyManager(rm=recipeManager.RecipeManager(**recipeManager.dbargs)) - From d13175241ad338afdfea7d9f5022c35e0f717e83 Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Sun, 25 Oct 2020 16:54:44 +0100 Subject: [PATCH 14/24] Move image_to_pixbuf to image_utils.py --- gourmet/image_utils.py | 13 +++++++ .../plugins/browse_recipes/icon_helpers.py | 39 ++++--------------- gourmet/tests/test_image_utils.py | 10 +++-- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/gourmet/image_utils.py b/gourmet/image_utils.py index f657154b0..8e149f46b 100644 --- a/gourmet/image_utils.py +++ b/gourmet/image_utils.py @@ -99,6 +99,19 @@ def pixbuf_to_image(pixbuf: Pixbuf) -> Image.Image: return image +def image_to_pixbuf(image: Image.Image) -> Pixbuf: + is_rgba = image.mode == 'RGBA' + rowstride = 4 if is_rgba else 3 + + return GdkPixbuf.Pixbuf.new_from_data(image.tobytes(), + GdkPixbuf.Colorspace.RGB, + is_rgba, + 8, + image.size[0], + image.size[1], + rowstride * image.size[0]) + + class ImageBrowser(Gtk.Dialog): def __init__(self, parent: Gtk.Window, uris: List[str]): Gtk.Dialog.__init__(self, title="Choose an image", diff --git a/gourmet/plugins/browse_recipes/icon_helpers.py b/gourmet/plugins/browse_recipes/icon_helpers.py index 2540d0e41..fea71af6f 100644 --- a/gourmet/plugins/browse_recipes/icon_helpers.py +++ b/gourmet/plugins/browse_recipes/icon_helpers.py @@ -1,16 +1,14 @@ -from gi.repository import GdkPixbuf, Gtk import os.path -# mentioning PIL explicitly helps py2exe -try: - from PIL import Image, ImageDraw -except ImportError: - import Image, ImageDraw -from gourmet.image_utils import bytes_to_pixbuf + +from gi.repository import GdkPixbuf, Gtk +from PIL import Image, ImageDraw + +from gourmet.image_utils import bytes_to_pixbuf, image_to_pixbuf from gourmet.gtk_extras.ratingWidget import star_generator curdir = os.path.split(__file__)[0] +ICON_SIZE = 125 -ICON_SIZE=125 def scale_pb (pb, do_grow=True): w = pb.get_width() @@ -28,29 +26,6 @@ def scale_pb (pb, do_grow=True): target_w = int(target * (float(w)/h)) return pb.scale_simple(target_w,target_h,GdkPixbuf.InterpType.BILINEAR) -def get_pixbuf_from_image (image): - - """Get a pixbuf from a PIL Image. - - By default, turn all white pixels transparent. - """ - - # TODO: This function should be moved to gourmet.image_extra.pixbuf_to_image - - is_rgba = image.mode=='RGBA' - if is_rgba: rowstride = 4 - else: rowstride = 3 - pb=GdkPixbuf.Pixbuf.new_from_data( - image.tobytes(), - GdkPixbuf.Colorspace.RGB, - is_rgba, - 8, - image.size[0], - image.size[1], - (is_rgba and 4 or 3) * image.size[0] #rowstride - ) - return pb - generic_recipe_image = scale_pb(GdkPixbuf.Pixbuf.new_from_file(os.path.join(curdir,'images','generic_recipe.png'))) preptime_image = GdkPixbuf.Pixbuf.new_from_file(os.path.join(curdir,'images','preptime.png')) @@ -142,7 +117,7 @@ def get_image (self, angle, color): ) d = ImageDraw.Draw(img) d.pieslice((10,10,ICON_SIZE-10,ICON_SIZE-10),-90,-90 + angle, color) - self.slices[(angle,color)] = get_pixbuf_from_image(img) + self.slices[(angle,color)] = image_to_pixbuf(img) return self.slices[(angle,color)] def get_time_image (self, time_in_seconds): diff --git a/gourmet/tests/test_image_utils.py b/gourmet/tests/test_image_utils.py index 1edeb5a40..4cf7b6d95 100644 --- a/gourmet/tests/test_image_utils.py +++ b/gourmet/tests/test_image_utils.py @@ -4,8 +4,8 @@ from PIL import Image, ImageChops from gourmet.image_utils import ( - bytes_to_image, bytes_to_pixbuf, image_to_bytes, make_thumbnail, - pixbuf_to_image, ThumbnailSize) + bytes_to_image, bytes_to_pixbuf, image_to_bytes, image_to_pixbuf, + make_thumbnail, pixbuf_to_image, ThumbnailSize) IMAGE = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a\x1c\x1c $.\' ",#\x1c\x1c(7),01444\x1f\'9=82<.342\xff\xdb\x00C\x01\t\t\t\x0c\x0b\x0c\x18\r\r\x182!\x1c!22222222222222222222222222222222222222222222222222\xff\xc0\x00\x11\x08\x00(\x009\x03\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\xff\xc4\x00\xb5\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05\x04\x04\x00\x00\x01}\x01\x02\x03\x00\x04\x11\x05\x12!1A\x06\x13Qa\x07"q\x142\x81\x91\xa1\x08#B\xb1\xc1\x15R\xd1\xf0$3br\x82\t\n\x16\x17\x18\x19\x1a%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xff\xc4\x00\x1f\x01\x00\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\xff\xc4\x00\xb5\x11\x00\x02\x01\x02\x04\x04\x03\x04\x07\x05\x04\x04\x00\x01\x02w\x00\x01\x02\x03\x11\x04\x05!1\x06\x12AQ\x07aq\x13"2\x81\x08\x14B\x91\xa1\xb1\xc1\t#3R\xf0\x15br\xd1\n\x16$4\xe1%\xf1\x17\x18\x19\x1a&\'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\xe4>V\xdaA\x18\xe4\xe3\xf2\xe9\xf5\xcdy\xf8\xacdhB\xcd\xea\xf6=\x8a*\x83v\xa3\x0en\xed\xff\x00\x91\xe6\x1a\xa6\xa3aa\xa8\x1b8ti.\x95c\x04\xb1l3\x12\xa1\xc7\xcb\xe9\xb7\'<\xe7\xda\xabL\xda<\xed<)a\x0b\x10\xf1\x88D\x91\xc9\x19\x19nA\xf9\xc6\xe6\xc9\xfb\xa3\x18\xe7\x92\x06k\xaf\xf1\x0e\x97\x05\xecwK\x1c&\x061$\x89r\x8aN\x19K\xa0\x19\xff\x00gh\'\x9e\x84\xf1\xc0\xae\x1fX\xf0\xce\xa5\xe1\xeb\x1b\xab\xaf\xb4\x19\xad\xb2\xe3.\xe4\x1d\x81\xd9Q\xb8a\xbb\x8d\xa7\x1d>`0y\xc7\x1d\xc8\xc0\xc8\x1b\xca0\x0c\xd8>\x98\xe7\x1dG\xb9\xack-m\xf5 Y_\x13\x81\x86Y0\x00u9F\xfa\x11\x95ls\xd3\x18\xebSx\x9d\xdd\xb4\x99\xf6\xc7\x1c\xa2\tR\xe8,\x8b\xb8\x18\xcepq\xdb\x9c\x83\x9cp\x0fj\xc2\x8c\'N\xb4c#v\x94]\xdfC\x90\x1al\xd6\xf3\xc14n&\x10a\xd8FD\xa9\x8c\xee\xc3\x1e\x98\xc7\\\x8cu\xe3\x83Oh\xed\xafn\x1eb\x02\xc8K\xb7\x99\x1a\xacJI#\x92\xb9 \x01\x9e\x8a=ES\xb2\xd4r\x0f\x90\x88\x19\x95\x91\xd5Qy\x18\x03;\x9b\xd7\xd3\x1c~&\xa4\x9e\xed\xadd\xcd\xc5\x8a\x1b|\xf9\x91\xc2\xdb\x95;g\x95 \xf4\xc089\xe7\xb7oe\xa9\xb7g\xb8\x9chJ\x1c\xd6\xd3\xf06\xc6\xa1\x05\xacN\xdat\xf1\xc4\x92H\xa08\x8b\xcc\x01\x9599e\xca\x82\xdb\x8f\x1d8\xe7\x8a\xed\x7f\xe1/\x97\xfe\x83W\xdf\xf8\x0c\x7f\xf8\xe5y\x9e\x95\x04z\x84\x90\xc2\xf3G\xb4\xfc\xca\xb1\x92\x19Opr9\xcf\xe3\xd3\xadv_\xf0\x8fC\xfd\xc7\xff\x00\xbf\xa7\xfck\x87\x13*P\x92S\xdc\xea\xa1JUax\xda\xc7\xab\xf8\x97\xc3\xd2\xeb2\xc5s\x04\x89\xbd\x13o\x96\xdcg\xa9\xe0\xfa\xf2k\x89\xb9\xf0V\xa3\xe6\xaf\x9dc\xba<\x82\xeb\xb3\x7f\xe5\x8c\xd1E^;\t\x18\xc9\xd4\x8bi\x9e^\x0f4\xadN\x92\x86\x8d\x14\xb5O\x03\xdd\\\x1f:\xde\xda\xee\x19@\xc0dF\xc0\x1f\x88\xe4U{%\xbc\xd2\xe5\xfb\'\x88bH\xe3\xc3\x08&*\x0bc\x8d\xc0\xa8\xfe\x13\x90N{\xe4v\xc2\x94W\r;\xca\x9f,\x9d\xce\x87\x8c\x95o\x8a(\xc9\xd4|\x05\x03y\x97\x9a=\xf8\x95\x18\x9d\xd1,\x8a\xeb\x9c\xf4\xdcH\xc68\x1d\xcf\xb9\xac4\x86\xe2\t>\xcf\xa8\x15a\x06\n\xae\xef0\x03\x9fO\xc4\x9e\xbe\xbf\x81Et\xd2\xafRW\x8c\x9d\xeckEZ\\\x8bc\xba\xf0\xa7\x83b\xd4\xb5e\x96+E\x8a CHW?"\xfdOs\xfa\xfeu\xeb\xff\x00\xd8Zg\xfc\xf8\xdb\xff\x00\xdf\xa1\xfe\x14Q]X*J\xa49\xe7\xabg\x0eg\x88\x9a\xae\xe9\xc7E\x1d\x8f\xff\xd9' # noqa @@ -44,5 +44,7 @@ def test_pixbuf_to_image(): assert isinstance(image, Image.Image) - - +def test_image_to_pixbuf(): + image = bytes_to_image(IMAGE) + pixbuf = image_to_pixbuf(image) + assert isinstance(pixbuf, Pixbuf) From dc24490ca58b415ab849a1024f9e4b0fef6e9136 Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Sun, 25 Oct 2020 18:49:22 +0100 Subject: [PATCH 15/24] Clean image handling in Browser plugin --- gourmet/plugins/browse_recipes/browser.py | 81 +++++++++---------- .../plugins/browse_recipes/icon_helpers.py | 17 +--- 2 files changed, 44 insertions(+), 54 deletions(-) diff --git a/gourmet/plugins/browse_recipes/browser.py b/gourmet/plugins/browse_recipes/browser.py index f127af7d8..1f1107bee 100644 --- a/gourmet/plugins/browse_recipes/browser.py +++ b/gourmet/plugins/browse_recipes/browser.py @@ -1,15 +1,20 @@ -from gi.repository import GdkPixbuf, GObject, Gtk import os.path -from gourmet.gglobals import DEFAULT_ATTR_ORDER, REC_ATTR_DIC -from gourmet.image_utils import bytes_to_pixbuf + +from gettext import gettext as _ +from gi.repository import GdkPixbuf, GObject, Gtk +from sqlalchemy.sql import and_, not_ + import gourmet.convert as convert +from gourmet.gglobals import DEFAULT_ATTR_ORDER, REC_ATTR_DIC from gourmet.gtk_extras.ratingWidget import star_generator -from sqlalchemy.sql import and_, or_, not_ -from gettext import gettext as _ -from .icon_helpers import attr_to_icon, get_recipe_image, get_time_slice, scale_pb +from gourmet.image_utils import bytes_to_pixbuf + +from .icon_helpers import (attr_to_icon, get_recipe_image, get_time_slice, + scale_pb) curdir = os.path.split(__file__)[0] + class RecipeBrowserView (Gtk.IconView): __gsignals__ = { @@ -19,7 +24,7 @@ class RecipeBrowserView (Gtk.IconView): GObject.TYPE_STRING,[GObject.TYPE_STRING]) } - def __init__ (self, rd): + def __init__(self, rd): self.rd = rd Gtk.IconView.__init__(self) self.set_selection_mode(Gtk.SelectionMode.MULTIPLE) @@ -27,15 +32,15 @@ def __init__ (self, rd): self.set_model() self.set_text_column(1) self.set_pixbuf_column(2) - self.connect('item-activated',self.item_activated_cb) + self.connect('item-activated', self.item_activated_cb) self.switch_model('base') self.path = ['base'] - def new_model (self): return Gtk.ListStore(str, # path - str, # text - GdkPixbuf.Pixbuf, # image - GObject.TYPE_PYOBJECT, - ) + def new_model(self): + return Gtk.ListStore(str, # path + str, # text + GdkPixbuf.Pixbuf, # image + GObject.TYPE_PYOBJECT) def switch_model (self, path, val=None): if path not in self.models: @@ -50,16 +55,17 @@ def build_model (self, path,val): else: self.build_recipe_model(path,val) - def build_base_model (self): + def build_base_model(self): m = self.models['base'] = self.new_model() self.set_model(m) - for itm in DEFAULT_ATTR_ORDER: - if itm in ['title','link','yields']: continue - pb = self.get_base_icon(itm) - m.append((itm,(REC_ATTR_DIC[itm]),pb,None)) + for item in DEFAULT_ATTR_ORDER: + if item in ['title', 'link', 'yields']: + continue + pb = self.get_base_icon(item) + m.append((item, (REC_ATTR_DIC[item]), pb, None)) def get_base_icon (self, itm): - return attr_to_icon.get(itm,attr_to_icon['category']) + return attr_to_icon.get(itm, attr_to_icon['category']) def get_pixbuf (self, attr,val): if attr=='category': @@ -90,18 +96,8 @@ def get_pixbuf (self, attr,val): else: return self.get_base_icon(attr) or self.get_base_icon('category') - def get_default_icon (self): - if hasattr(self,'default_icon'): - return self.default_icon - else: - #from gourmet.gglobals import imagedir - path = os.path.join(curdir,'images','generic_category.png') - self.default_icon = scale_pb(GdkPixbuf.Pixbuf.new_from_file(path),do_grow=True) - return self.default_icon - - - def convert_val (self, attr, val): - if attr in ['preptime','cooktime']: + def convert_val(self, attr, val) -> str: + if attr in ['preptime', 'cooktime']: if val: return convert.seconds_to_timestring(val) else: @@ -138,10 +134,9 @@ def build_recipe_model (self, path, val): searches = [{'column':'deleted','operator':'=','search':False}] path = path.split('>') while path: - textval = path.pop() + _ = path.pop() attr = path.pop() if val is None: - val = None searches.append({'column':attr,'search':val,'operator':'='}) else: searches.append({'column':attr,'search':val}) @@ -190,25 +185,27 @@ def back (self): self.ahead = self.path.pop() self.switch_model(self.path[-1]) -class RecipeBrowser (Gtk.VBox): - def __init__ (self, rd): +class RecipeBrowser(Gtk.VBox): + + def __init__(self, rd): Gtk.VBox.__init__(self) self.view = RecipeBrowserView(rd) self.buttons = [] self.button_bar = Gtk.HBox() self.button_bar.set_spacing(6) - # self.pack_start(self.button_bar,expand=False) self.pack_start(self.button_bar, False, False, 0) sw = Gtk.ScrolledWindow() - sw.set_policy(Gtk.PolicyType.AUTOMATIC,Gtk.PolicyType.AUTOMATIC) + sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(self.view) self.pack_start(sw, True, True, 0) home_button = Gtk.Button(stock=Gtk.STOCK_HOME) self.button_bar.pack_start(home_button, False, False, 0) - home_button.connect('clicked',self.home); home_button.show() - self.view.connect('path-selected',self.path_selected_cb) - self.view.show(); sw.show() + home_button.connect('clicked', self.home) + home_button.show() + self.view.connect('path-selected', self.path_selected_cb) + self.view.show() + sw.show() def home (self, *args): self.view.set_path('base') @@ -225,7 +222,9 @@ def path_selected_cb (self, view, path): def append_button (self, path): if '>' in path: - txt = self.view.convert_val(*path.split('>')) + attribute, value = path.split('>') + value = int(value) + txt = self.view.convert_val(attribute, value) else: txt = path self.buttons.append(Gtk.Button(REC_ATTR_DIC.get(txt,txt))) diff --git a/gourmet/plugins/browse_recipes/icon_helpers.py b/gourmet/plugins/browse_recipes/icon_helpers.py index fea71af6f..9c0d3399c 100644 --- a/gourmet/plugins/browse_recipes/icon_helpers.py +++ b/gourmet/plugins/browse_recipes/icon_helpers.py @@ -8,6 +8,8 @@ curdir = os.path.split(__file__)[0] ICON_SIZE = 125 +PREP = 1 +COOK = 2 def scale_pb (pb, do_grow=True): @@ -101,9 +103,9 @@ def get_recipe_image (rec): ) return pb -class PiePixbufGenerator: - '''Generate Pie-chart style pixbufs representing circles''' +class PiePixbufGenerator: + """Generate Pie-chart style pixbufs representing circles""" def __init__ (self): self.slices = {} @@ -135,17 +137,6 @@ def get_time_image (self, time_in_seconds): make_pie_slice = pie_generator.get_image get_time_slice = pie_generator.get_time_image -def make_time_icon (text): - img = Image.new('RGBA', - (ICON_SIZE,ICON_SIZE), - 255 # background - ) - d = ImageDraw.Draw(img) - #Thosed.text( - -PREP = 1 -COOK = 2 - def make_preptime_icon (preptime): return make_time_icon(preptime,mode=PREP) From 119a6e1a6b02095d7f725025d1dde419782c96e5 Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Sun, 25 Oct 2020 20:08:24 +0100 Subject: [PATCH 16/24] Recipe Browser: set IconView max width to ICON_SIZE Reduce the huge blank space surrounding icons --- gourmet/plugins/browse_recipes/browser.py | 23 ++++++++++--------- .../plugins/browse_recipes/icon_helpers.py | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/gourmet/plugins/browse_recipes/browser.py b/gourmet/plugins/browse_recipes/browser.py index 1f1107bee..d51c12855 100644 --- a/gourmet/plugins/browse_recipes/browser.py +++ b/gourmet/plugins/browse_recipes/browser.py @@ -10,24 +10,25 @@ from gourmet.image_utils import bytes_to_pixbuf from .icon_helpers import (attr_to_icon, get_recipe_image, get_time_slice, - scale_pb) + ICON_SIZE, scale_pb) curdir = os.path.split(__file__)[0] -class RecipeBrowserView (Gtk.IconView): +class RecipeBrowserView(Gtk.IconView): __gsignals__ = { - 'recipe-selected':(GObject.SignalFlags.RUN_LAST, - GObject.TYPE_INT,[GObject.TYPE_INT]), - 'path-selected':(GObject.SignalFlags.RUN_LAST, - GObject.TYPE_STRING,[GObject.TYPE_STRING]) - } + 'recipe-selected': (GObject.SignalFlags.RUN_LAST, + GObject.TYPE_INT, [GObject.TYPE_INT]), + 'path-selected': (GObject.SignalFlags.RUN_LAST, + GObject.TYPE_STRING, [GObject.TYPE_STRING]) + } def __init__(self, rd): self.rd = rd - Gtk.IconView.__init__(self) - self.set_selection_mode(Gtk.SelectionMode.MULTIPLE) + super().__init__() + self.set_selection_mode(Gtk.SelectionMode.SINGLE) + self.set_item_width(ICON_SIZE) self.models = {} self.set_model() self.set_text_column(1) @@ -64,8 +65,8 @@ def build_base_model(self): pb = self.get_base_icon(item) m.append((item, (REC_ATTR_DIC[item]), pb, None)) - def get_base_icon (self, itm): - return attr_to_icon.get(itm, attr_to_icon['category']) + def get_base_icon(self, item): + return attr_to_icon.get(item, attr_to_icon['category']) def get_pixbuf (self, attr,val): if attr=='category': diff --git a/gourmet/plugins/browse_recipes/icon_helpers.py b/gourmet/plugins/browse_recipes/icon_helpers.py index 9c0d3399c..ffa284223 100644 --- a/gourmet/plugins/browse_recipes/icon_helpers.py +++ b/gourmet/plugins/browse_recipes/icon_helpers.py @@ -7,7 +7,7 @@ from gourmet.gtk_extras.ratingWidget import star_generator curdir = os.path.split(__file__)[0] -ICON_SIZE = 125 +ICON_SIZE = 126 PREP = 1 COOK = 2 From 679604947a723e084641fece648119061514fb6c Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Mon, 26 Oct 2020 14:54:37 +0100 Subject: [PATCH 17/24] Fix loading of thumbnails as icons in Recipe Browser A Python3 issue where strings needn't be converted to bytes --- gourmet/plugins/browse_recipes/browser.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/gourmet/plugins/browse_recipes/browser.py b/gourmet/plugins/browse_recipes/browser.py index d51c12855..6d3855e49 100644 --- a/gourmet/plugins/browse_recipes/browser.py +++ b/gourmet/plugins/browse_recipes/browser.py @@ -68,28 +68,30 @@ def build_base_model(self): def get_base_icon(self, item): return attr_to_icon.get(item, attr_to_icon['category']) - def get_pixbuf (self, attr,val): + def get_pixbuf(self, attr: str, val: str) -> GdkPixbuf.Pixbuf: if attr=='category': tbl = self.rd.recipe_table.join(self.rd.categories_table) col = self.rd.categories_table.c.category - if hasattr(self,'category_images'): - stment = and_(col == val.encode(), self.rd.recipe_table.c.image != None, + if hasattr(self, 'category_images'): + stment = and_(col == val, self.rd.recipe_table.c.image != None, self.rd.recipe_table.c.image != bytes(), not_(self.rd.recipe_table.c.title.in_(self.category_images))) else: - stment = and_(col == val.encode(), self.rd.recipe_table.c.image != None, + stment = and_(col == val, self.rd.recipe_table.c.image != None, self.rd.recipe_table.c.image != bytes()) result = tbl.select(stment,limit=1).execute().fetchone() - if not hasattr(self,'category_images'): self.category_images = [] - if result: self.category_images.append(result.title) - elif attr=='rating': + if not hasattr(self, 'category_images'): + self.category_images = [] + if result: + self.category_images.append(result.title) + elif attr == 'rating': return star_generator.get_pixbuf(val) - elif attr in ['preptime','cooktime']: + elif attr in ['preptime', 'cooktime']: return get_time_slice(val) else: tbl = self.rd.recipe_table - col = getattr(self.rd.recipe_table.c,attr) - stment = and_(col == val.encode(), self.rd.recipe_table.c.image != None, + col = getattr(self.rd.recipe_table.c, attr) + stment = and_(col == val, self.rd.recipe_table.c.image != None, self.rd.recipe_table.c.image != bytes()) result = tbl.select(stment,limit=1).execute().fetchone() if result and result.thumb: From d94be3b1bc146c3d535f7a6161ae489f1ccfb6f1 Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Mon, 26 Oct 2020 18:48:33 +0100 Subject: [PATCH 18/24] Fix preview rendering in ImageSelectorDialog --- gourmet/gtk_extras/dialog_extras.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gourmet/gtk_extras/dialog_extras.py b/gourmet/gtk_extras/dialog_extras.py index 606277969..5bb1e8f35 100644 --- a/gourmet/gtk_extras/dialog_extras.py +++ b/gourmet/gtk_extras/dialog_extras.py @@ -9,7 +9,7 @@ from . import optionTable from gourmet.gdebug import debug -from gourmet.image_utils import make_thumbnail +from gourmet.image_utils import image_to_pixbuf, make_thumbnail H_PADDING=12 Y_PADDING=12 @@ -1126,7 +1126,7 @@ def update_preview (self, *args): thumbnail = make_thumbnail(uri) if thumbnail is not None: - self.preview.set_from_pixbuf(thumbnail) + self.preview.set_from_pixbuf(image_to_pixbuf(thumbnail)) self.preview.show() else: self.preview.hide() From da3aba665ebc53b1529f1db7c801650e765b629c Mon Sep 17 00:00:00 2001 From: Enno Hermann Date: Tue, 27 Oct 2020 10:54:31 +0100 Subject: [PATCH 19/24] Fix GtkButtonBoxStyle enum GTK 3 does not have DEFAULT_STYLE, the default now is EDGE: https://developer.gnome.org/gtk3/stable/GtkButtonBox.html#GtkButtonBoxStyle --- gourmet/ui/timerDialog.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gourmet/ui/timerDialog.ui b/gourmet/ui/timerDialog.ui index 3b93da47f..18b18ce74 100644 --- a/gourmet/ui/timerDialog.ui +++ b/gourmet/ui/timerDialog.ui @@ -232,7 +232,7 @@ True - GTK_BUTTONBOX_DEFAULT_STYLE + GTK_BUTTONBOX_EDGE 0 @@ -406,7 +406,7 @@ True - GTK_BUTTONBOX_DEFAULT_STYLE + GTK_BUTTONBOX_EDGE 12 From a6697132493c4340c32c60872a113e51e1fe8fca Mon Sep 17 00:00:00 2001 From: Enno Hermann Date: Tue, 27 Oct 2020 11:24:45 +0100 Subject: [PATCH 20/24] Consolidate sound handling to gst --- INSTALL.md | 10 +++++----- gourmet/__init__.py | 1 + gourmet/sound.py | 28 ++++++++++------------------ gourmet/sound_gst.py | 19 ------------------- gourmet/sound_pyglet.py | 16 ---------------- gourmet/sound_windows.py | 11 ----------- 6 files changed, 16 insertions(+), 69 deletions(-) delete mode 100644 gourmet/sound_gst.py delete mode 100644 gourmet/sound_pyglet.py delete mode 100644 gourmet/sound_windows.py diff --git a/INSTALL.md b/INSTALL.md index c16475603..199710e8d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,9 +1,9 @@ # Installation -Gourmet is currently available in the form of Flatpak and Python wheel. -We recommend that you install it from the Flatpak. +Gourmet is currently available in the form of Flatpak and Python wheel. +We recommend that you install it from the Flatpak. In both cases, you will need an internet connection. -**We strongly recommend that you make a backup of your recipe database.** +**We strongly recommend that you make a backup of your recipe database.** As Gourmet is still in early stage of (re)development, make a backup of your recipe database, typically found under `$HOME/.gourmet/recipe.db`: ```sh @@ -22,7 +22,7 @@ sudo apt-get install flatpak As Gourmet is still under active development, the flatpak is not available from Flathub, and instead must be [downloaded and installed manually](https://github.com/kirienko/gourmet/releases/tag/v1-alpha2). -In a terminal, execute the following: +In a terminal, execute the following: ```sh flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo sudo flatpak install gourmet-2db9db8f.flatpak @@ -62,7 +62,7 @@ Install the following packages from `apt`: ```sh sudo apt-get update -sudo apt-get install --no-install-recommends python3-argcomplete python3-gi python3-gi-cairo gir1.2-gtk-3.0 libgirepository1.0-dev libcairo2-dev enchant python3-bs4 python3-ebooklib python3-keyring python3-lxml python3-pil python3-cairo python3-enchant python3-gi python3-gtkspellcheck python3-requests python3-reportlab python3-selenium python3-setuptools python3-sqlalchemy python3-pip python3-toml gir1.2-poppler-0.18 ``` +sudo apt-get install --no-install-recommends python3-argcomplete python3-gi python3-gi-cairo gir1.2-gtk-3.0 libgirepository1.0-dev libcairo2-dev enchant python3-bs4 python3-ebooklib python3-keyring python3-lxml python3-pil python3-cairo python3-enchant python3-gi python3-gst-1.0 python3-gtkspellcheck python3-requests python3-reportlab python3-selenium python3-setuptools python3-sqlalchemy python3-pip python3-toml gir1.2-poppler-0.18 ``` Then, install dependencies from the Python repository: ```sh diff --git a/gourmet/__init__.py b/gourmet/__init__.py index be86280a4..a8db3eed2 100644 --- a/gourmet/__init__.py +++ b/gourmet/__init__.py @@ -1,4 +1,5 @@ from gi import require_version require_version("Gdk", "3.0") +require_version("Gst", "1.0") require_version("Gtk", "3.0") require_version("Pango", "1.0") diff --git a/gourmet/sound.py b/gourmet/sound.py index 6b5e197a6..724eb02b2 100644 --- a/gourmet/sound.py +++ b/gourmet/sound.py @@ -1,19 +1,11 @@ -try: - from .sound_pyglet import Player -except ImportError: - print('No pyglet player') - try: - from .sound_gst import Player - except ImportError: - print('No gst player') - try: - from .sound_windows import Player - except ImportError: - print('No windows player') - import sys - class Player: - """Fallback player""" - def play_file (self,path): - print('No player installed -- beeping instead') - for n in range(5): sys.stdout.write('\a'); sys.stdout.flush() +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): + self.player.set_state(Gst.State.NULL) + self.player.set_property('uri', 'file://' + path) + self.player.set_state(Gst.State.PLAYING) diff --git a/gourmet/sound_gst.py b/gourmet/sound_gst.py deleted file mode 100644 index 5cd89e3da..000000000 --- a/gourmet/sound_gst.py +++ /dev/null @@ -1,19 +0,0 @@ -from gi import require_version -try: - require_version('Gst', '1.0') -except ValueError as e: - # gourmet.sound catches ImportError - raise ImportError("Gst not available") from e -from gi.repository import Gst - -class Player: - def __init__ (self): - self.player = Gst.ElementFactory.make('playbin2','player') - - def play_file (self,path): - self.player.set_state(Gst.State.NULL) - self.player.set_property('uri','file://'+path) - self.player.set_state(Gst.State.PLAYING) - - def stop_play (self,path): - self.player.set_state(Gst.State.NULL) diff --git a/gourmet/sound_pyglet.py b/gourmet/sound_pyglet.py deleted file mode 100644 index 41da613a3..000000000 --- a/gourmet/sound_pyglet.py +++ /dev/null @@ -1,16 +0,0 @@ -import pyglet - -class Player: - def __init__ (self): - pass - - def play_file (self,path): - self.source = pyglet.media.load(path,streaming=False) - self.source.play() - - def stop_play (self,path): - pass - -if __name__ == '__main__': - p = Player() - p.play_file('../data/sound/phone.wav') diff --git a/gourmet/sound_windows.py b/gourmet/sound_windows.py deleted file mode 100644 index c65d6cd43..000000000 --- a/gourmet/sound_windows.py +++ /dev/null @@ -1,11 +0,0 @@ -import winsound - -class Player: - def __init__ (self): - pass - - def play_file (self,path): - winsound.PlaySound(path,winsound.SND_FILENAME) - - def stop_play (self,path): - pass From ec02c01d5ce6ae3528874ef8d19047a03c6e155b Mon Sep 17 00:00:00 2001 From: Enno Hermann Date: Tue, 27 Oct 2020 16:11:35 +0100 Subject: [PATCH 21/24] Add python3-gst-1.0 dependency --- .github/workflows/build.yml | 2 +- .github/workflows/tests.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc80a8f59..2f5798607 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: sudo apt-get update -q && sudo apt-get install --no-install-recommends -y xvfb python3-dev python3-gi python3-gi-cairo gir1.2-gtk-3.0 libgirepository1.0-dev libcairo2-dev - intltool enchant python3-enchant + intltool enchant python3-enchant python3-gst-1.0 - name: Install dependencies run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b6d84fe56..a76857b6e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,6 +25,7 @@ jobs: --no-install-recommends -y xvfb python3-dev python3-gi python3-gi-cairo gir1.2-gtk-3.0 libgirepository1.0-dev gir1.2-poppler-0.18 libcairo2-dev enchant python3-enchant intltool + python3-gst-1.0 - name: Install dependencies run: | From 6669cbe46a3f31dc4a96e4c95ad4447fa6606e30 Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Tue, 27 Oct 2020 17:04:39 +0100 Subject: [PATCH 22/24] Add Poppler to Flatpak --- .flatpak/io.github.thinkle.Gourmet.yml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.flatpak/io.github.thinkle.Gourmet.yml b/.flatpak/io.github.thinkle.Gourmet.yml index dcb581e9e..db670420d 100644 --- a/.flatpak/io.github.thinkle.Gourmet.yml +++ b/.flatpak/io.github.thinkle.Gourmet.yml @@ -10,18 +10,29 @@ finish-args: - --socket=pulseaudio - --share=network modules: + - name: poppler + buildsystem: cmake-ninja + config-opts: + - -DENABLE_UTILS=OFF + - -DENABLE_CPP=OFF + - -DENABLE_QT5=OFF + sources: + - url: https://poppler.freedesktop.org/poppler-20.10.0.tar.xz + sha256: 434ecbbb539c1a75955030a1c9b24c7b58200b7f68d2e4269e29acf2f8f13336 + type: archive + - name: cpython sources: - type: archive - url: https://www.python.org/ftp/python/3.8.4/Python-3.8.4.tar.xz - sha256: 5f41968a95afe9bc12192d7e6861aab31e80a46c46fa59d3d837def6a4cd4d37 + url: https://www.python.org/ftp/python/3.8.4/Python-3.8.4.tar.xz + sha256: 5f41968a95afe9bc12192d7e6861aab31e80a46c46fa59d3d837def6a4cd4d37 - name: intltool buildsystem: autotools sources: - type: archive - url: https://launchpad.net/intltool/trunk/0.51.0/+download/intltool-0.51.0.tar.gz - sha256: 67c74d94196b153b774ab9f89b2fa6c6ba79352407037c8c14d5aeb334e959cd + url: https://launchpad.net/intltool/trunk/0.51.0/+download/intltool-0.51.0.tar.gz + sha256: 67c74d94196b153b774ab9f89b2fa6c6ba79352407037c8c14d5aeb334e959cd - name: gourmet buildsystem: simple @@ -29,13 +40,13 @@ modules: build-args: - --share=network build-commands: - - pip3 install pyenchant pygobject + - python3 -m pip install --upgrade pip + - pip3 install pyenchant pygobject Sphinx - pip3 install -r requirements.txt - python3 setup.py build_i18n - python3 setup.py install --prefix=/app - install -Dm644 .flatpak/io.github.thinkle.Gourmet.desktop -t /app/share/applications/ - install -Dm644 .flatpak/io.github.thinkle.Gourmet.svg -t /app/share/icons/hicolor/scalable/apps/ - sources: - type: git - url: https://github.com/kirienko/gourmet + url: https://github.com/kirienko/gourmet From 472b6f30f2157f888b324d602cc326f5f13bcf9e Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Tue, 27 Oct 2020 19:31:46 +0100 Subject: [PATCH 23/24] Fix printing of dependent recipes --- gourmet/exporters/exporter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gourmet/exporters/exporter.py b/gourmet/exporters/exporter.py index aa9819b81..9e75a4ad7 100644 --- a/gourmet/exporters/exporter.py +++ b/gourmet/exporters/exporter.py @@ -540,8 +540,7 @@ def append_referenced_recipes (self): ) for ref in reffed: rec = self.rd.get_rec(ref.refid) - if not rec in self.recipes: - print('Appending recipe ',rec.title,'referenced in ',r.title) + if rec is not None and not rec in self.recipes: self.recipes.append(rec) @pluggable_method From ac985d03db9a20c53a24222402c446cebb42a7ec Mon Sep 17 00:00:00 2001 From: Enno Hermann Date: Tue, 27 Oct 2020 22:26:29 +0100 Subject: [PATCH 24/24] Fix shopping list buttons and dialogs --- gourmet/GourmetRecipeManager.py | 6 ++++-- gourmet/reccard.py | 4 ++-- gourmet/shopgui.py | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/gourmet/GourmetRecipeManager.py b/gourmet/GourmetRecipeManager.py index 331f80e38..1682af0b3 100644 --- a/gourmet/GourmetRecipeManager.py +++ b/gourmet/GourmetRecipeManager.py @@ -746,7 +746,8 @@ def shop_recs (self, *args): parent=self.app.get_toplevel(), digits=2) if not mult: - mult = float(1) + debug('getNumber cancelled', 2) + return d=self.sl.getOptionalIngDic(self.rd.get_ings(r),mult,self.prefs) self.sl.addRec(r,mult,d) self.sl.show() @@ -1029,7 +1030,8 @@ def setup_actions (self): #None,None,self.email_recs), ('BatchEdit',None,_('Batch _edit recipes'), 'E',None,self.batch_edit_recs), - ('ShopRec','add-to-shopping-list',None,None,None,self.shop_recs) + ('ShopRec', 'add-to-shopping-list', _('Add to Shopping List'), + 'B', None, self.shop_recs) ]) self.mainActionGroup = Gtk.ActionGroup(name='MainActions') diff --git a/gourmet/reccard.py b/gourmet/reccard.py index 5e74149ec..55ccd97dc 100644 --- a/gourmet/reccard.py +++ b/gourmet/reccard.py @@ -246,7 +246,8 @@ def setup_actions (self): # None,None,self.email_cb), ('Print',Gtk.STOCK_PRINT,_('Print recipe'), 'P',None,self.print_cb), - ('ShopRec','add-to-shopping-list',None,None,None,self.shop_for_recipe_cb), + ('ShopRec', 'add-to-shopping-list', _('Add to Shopping List'), + 'B', None, self.shop_for_recipe_cb), ('ForgetRememberedOptionals',None,_('Forget remembered optional ingredients'), None,_('Before adding to shopping list, ask about all optional ingredients, even ones you previously wanted remembered'),self.forget_remembered_optional_ingredients), ]) @@ -3122,4 +3123,3 @@ def getYieldSelection (rec, parent=None): return yd.run() except: return 1 - diff --git a/gourmet/shopgui.py b/gourmet/shopgui.py index d6e5e5182..8b6387fd4 100644 --- a/gourmet/shopgui.py +++ b/gourmet/shopgui.py @@ -838,7 +838,8 @@ def clear_recipes (self, *args): self.recs.__delitem__(t.id) debug("clear removed %s"%t,3) self.reset() - elif de.getBoolean(label=_("No recipes selected. Do you want to clear the entire list?")): + elif de.getBoolean(label=_("No recipes selected. Do you want to clear the entire list?"), + cancel=False): self.recs = {} self.extras = [] self.reset()