Source code for gui.projectmgr

#
##
##  SPDX-FileCopyrightText: © 2007-2024 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.5  (Thu Feb  8 19:11:13 CET 2024)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  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 3 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, see http://www.gnu.org/licenses/.
##
"""Project menu.

Menu for handling Project contents and Project files.
"""
import pyformex as pf
from pyformex.project import SORTKEY
from pyformex.gui import guiscript as pg
from pyformex.gui.widgets import QtCore, _I, _C, _H
from pyformex.gui.annotations import Annotations

##################### handle project files ##########################


[docs]def askProjectFilename(fn=None, exist=False, caption='Select Project File'): """Ask the user for a Project file. """ if fn is None: fn = pf.PF.filename if fn is None: fn = '.' return pf.askFilename(fn, 'project', auto_suffix=True, caption=caption)
[docs]def openProject(fn=None, exist=False, read_only=False): """Open a (new or old) Project file. Ask the user for a Project file name and open the Project. This loads the project data and sets the selected file as the target for Project save operations. If a Project was already open, it is saved and closed first. Parameters ---------- fn: :term:`path_like` Returns ------- Project | None The opened Project, or None if the user canceled the dialog. """ fn = askProjectFilename(fn=fn, exist=exist, caption='Open Project') if not fn: return print(f"Opening project{fn}") with pg.busyCursor(): # loading may take a while closeProject() try: proj = pf.project.Project(fn) except Exception as e: pf.error(f"..\n\nCould not open project file {fn} " f"due to the following error:\n {e}") proj = None if proj is not None: set_project(proj)
[docs]def set_project(proj): """Make the specified project the current project. The current project is thrown away (make sure you have saved it if needed) and the provided project becomes the current project. Parameters ---------- proj: Project A Project """ if not isinstance(proj, pf.project.Project): raise ValueError("Expected a Project") pf.PF = proj if pf.PM: pf.PM.close() pf.GUI.setcurproj(pf.PF.filename) pm = projectmanager() pm.refresh() pm.list_all()
[docs]def importProject(fn=None): """Import an existing project. Ask the user to select an existing project file, and then import all or selected data from it into the current project. """ fn = askProjectFilename(fn=fn, exist=True, caption='Import Project') if not fn: return proj = pf.project.Project(fn) if proj: # only if non-empty keys = proj.contents() res = pg.askItems([ _I('mode', choices=['All', 'Defined', 'Undefined', 'Selected', 'None'], itemtype='radio'), _I('selected', choices=keys, itemtype='select'), ], caption='Select variables to import') if res: mode = res['mode'][0] if mode == 'A': pass elif mode == 'D': proj = pf.utils.selectDict(proj, pf.PF) elif mode == 'U': proj = pf.utils.removeDict(proj, pf.PF) elif mode == 'S': proj = pf.utils.selectDict(proj, res['selected']) elif mode == 'N': return print("Importing symbols:", sorted(proj.keys())) pf.PF.update(proj) pm = projectmanager() pm.refresh() pm.list_all()
[docs]def saveAsProject(fn=None): """Save the current project under a new name. A new project file is created, with the contents of the current project. The new project file becomes the current one. Returns ------- bool True if the project was saved, False if not (because the user canceled the dialog or didn't allow overwriting an existing file). """ fn = askProjectFilename(fn=fn, caption='Save Project As') if not fn: return False if fn.exists() and not pg.ack( f"Project file {fn} already exists, overwrite?"): return False print(f"Saving Project to file {fn}") pf.PF.filename = fn pf.GUI.setcurproj(pf.PF.filename) with pg.busyCursor(): # saving may take a while pf.PF.save() return True return False
[docs]def saveProject(): """Save the current project. If the current project has no filename, this is equivalent to :func:`saveAsProject`. It the projects has a filename and the contents has changed since the last save, it is saved to that file. """ if pf.PF.filename is None: return saveAsProject() else: if pf.PF.hits: print("Saving Project contents", pf.PF.contents()) # Always put the current version in projects saved from file menu! pf.PF.signature = pf.fullversion() with pg.busyCursor(): pf.PF.save() return True return False else: return True
[docs]def closeProject(save=None, clear=None): """Close the current project, possibly saving it. After closing, the current project is empty and not connected to a project file. Parameters ---------- save: bool, optional If True or False, determines whether the project should be saved prior to closing it. The default is to ask it from the user if there are unsaved changes in the project and the project is connected to a project file. - `clear`: None, True or False. """ if pf.PF.hits: if save is None: save = pg.ack("Save the current project before closing it?") print(f"Closing project {pf.PF.filename} ({save=})") if save: saveProject() pf.PF.filename = None pf.PF.hits = 0 pf.GUI.setcurproj(pf.PF.filename) pf.PF.clear() projectmanager().refresh()
##################### handle project contents ########################## def fmt_bbox(bb): import numpy as np return 'BBox(' + np.array2string(bb, separator=', ').replace('\n', '') + ')'
[docs]def objectInfo(obj, info): """Return some info about an object""" if info == 'Bbox': s = f"({obj.__class__.__name__}): {fmt_bbox(pf.bbox(obj))}" elif info == 'SurfaceType' and hasattr(obj, 'surfaceType'): manifold, orientable, closed, mincon, maxcon = obj.surfaceType() if not manifold: s = "is not a manifold" else: s_closed = 'a closed' if closed else 'an open' s_orient = 'orientable' if orientable else 'non-orientable' s = f"is {s_closed} {s_orient} manifold" elif info == 'SurfaceStats' and hasattr(obj, 'stats'): s = obj.stats() else: s = '' return s
[docs]class ProjectManager(pg.Dialog): """Project Manager""" _sortmethods = { 'No sorting': SORTKEY.NONE, 'Name': SORTKEY.NAME, 'Type, Name': SORTKEY.TYPENAME, } _exporttypes = ['pzf', 'pyf'] if pf.Module.has('h5py'): _exporttypes.append('hdf5') def __init__(self, project, fname='public'): self.proj = project self.dial_actions = { 'Accept': self.accept_sel, 'Cancel': self.cancel_sel, 'Hide': self.hide, 'Refresh': self.refresh, 'Select All': self.select_all, 'Deselect All': self.deselect_all, } self.proj_actions = { 'Open': openProject, 'Save': saveProject, 'Save As': saveAsProject, 'Import': importProject, 'Close': closeProject, 'Clear': self.clear_proj, 'List All': self.list_all, 'List Matches': self.list_matches, } self.sel_actions = { 'List': self.list_sel, 'Draw': self.draw_sel, 'Delete': self.delete_sel, 'Keep': self.keep_sel, 'Invert': self.invert_sel, 'Rename': self.rename_sel, # 'Edit': self.edit_sel, 'Export': self.export_sel, 'Object': self.print_sel, 'Bbox': (self.print_info, 'Bbox'), 'SurfaceType': (self.print_info, 'SurfaceType'), 'SurfaceStats': (self.print_info, 'SurfaceStats'), } # NOTE: filter has to come after objects because it sets it human, ignore, name, typ = self.proj.sortkey.split() typename = self.proj.sortkey & SORTKEY.TYPENAME sort = pf.utils.rev_lookup(self._sortmethods, typename) items = [ _C('', [ _I('_objects', itemtype='label', text='Filtered contents / Selection:'), _I('objects', itemtype='select', text='', choices=self.proj.contents(fname), empty_ok=True, maxh=-2), ], stretch=False), _C('', [ _I('dial_actions', text='Dialog:', itemtype='label'), _I('paction', choices=list(self.dial_actions.keys()), itemtype='push', text='', func=self.on_dial_action, count=3, checkable=False), _I('proj_actions', text='Project:', itemtype='label'), _I('paction', choices=list(self.proj_actions.keys()), itemtype='push', text='', func=self.on_proj_action, count=3, checkable=False), _I('sel_actions', text='Selection:', itemtype='label'), _I('action', choices=list(self.sel_actions.keys()), itemtype='push', text='', func=self.on_sel_action, count=3, checkable=False), _I('filter', fname, text='Filter', # BELOW 'objects' !! choices=self.proj.available_filters(), func=self.refresh, spacer='r'), _I('like', '', func=self.refresh, text='Names matching', tooltip="A regular expression to match the full object key"), _I('clas', '', text='Type(s)', tooltip="A class or tuple of classes"), ]), _H('', [ _I('display', self.proj.display, text='Display', choices=list(self.proj._display.keys()), tooltip="How to display the object", func=self.refresh_choices, spacer=''), _I('sort', sort, text='Sort', choices=list(self._sortmethods.keys()), tooltip="How to sort the contents", func=self.refresh_choices, spacer='r'), _I('human', human, text='Human sort', tooltip="Use human sorting?", func=self.refresh_choices, spacer='r'), _I('ignore', ignore, text='Ignore case', tooltip="How to sort the contents", func=self.refresh_choices, spacer='r'), ]), # _H('', [ # ]), ] enablers = [ # ('clas', '**custom**', 'customclas') ] self._no_callback = False super().__init__(caption='Project Manager', items=items, enablers=enablers, actions=[], message='Project:') def refresh_choices(self, item=None): # print(f"Refresh from {item}") # we need the try because this function may be called # before all fields are constructed. # TODO: try to do late activation of widget callback functions if self._no_callback: return try: fname = self['filter'].value() except KeyError: fname = None try: like = self['like'].value() if not like: like = None except KeyError: like = None try: clas = self['clas'].value() if clas: try: clas = eval(clas) except Exception: clas = None else: clas = None except AttributeError as e: pf.error(str(e)) clas = None except KeyError: clas = None try: sort = self._sortmethods[self['sort'].value()] human = self['human'].value() ignore = self['ignore'].value() sort = SORTKEY.combine(sort, human=human, ignore=ignore) except KeyError: sort = self.proj.sortkey try: display = self['display'].value() except KeyError: display = self.proj.display self['objects'].setChoices(self.proj.contents( fname=fname, clas=clas, like=like, sort=sort, display=display)) def refresh(self, item=None): names = self.selection self.refresh_choices(item) self.selection = names
[docs] def closeEvent(self, event): # print("Closing Project Manager") global pm pf.PM = pm = None event.accept()
def do_action(self, button, actions): action = button.text() func = actions[action] if isinstance(func, tuple): func, *args = func func(*args) else: func() def on_dial_action(self, button): self.do_action(button, self.dial_actions) def on_proj_action(self, button): self.do_action(button, self.proj_actions) def on_sel_action(self, button): sel = self.selection if not sel: pf.warning("You have to make a selection first") return self.do_action(button, self.sel_actions) @property def choices(self): return [self.proj.strip_type(n) for n in self['objects']._choices_] @property def selection(self): """Return names of selected objects""" return [self.proj.strip_type(n) for n in self['objects'].value()] @selection.setter def selection(self, names): """Set the selected objects""" displ_names = [n for n in self['objects']._choices_ if self.proj.strip_type(n) in names] self['objects'].setSelected(displ_names, excl=True) @property def sel_values(self): """Returns selected objects""" return [self.proj[name] for name in self.selection]
[docs] def sel_items(self): """Returns selected items""" return zip(self.selection, self.sel_values)
# TODO: can this be implemented by setting a persistent filter in Project
[docs] def sel_dict(self): """Return a dict with the selected objects""" return dict(self.sel_items())
########### Project actions ############
[docs] def list_all(self): """Print all Project keys""" print(self.proj.contents('all', display='Name'))
[docs] def list_matches(self): """Print all names passing filter""" print(self.choices)
[docs] def clear_proj(self): """Clear the contents of the current project""" if pf.ack("Are you sure you want to delete everything?"): self.proj.clear() self.selection = [] self.refresh()
########## Selection actions ########### def list_sel(self): print(self.selection) def print_sel(self): self.proj.pprint('Project selection:\n', self.selection)
[docs] def print_info(self, info): """Print some info about an object""" print(f"{info} info for selection:") for name, obj in self.sel_items(): s = objectInfo(obj, info) if s: print(f"* {name} {s}") if info == 'Bbox': bb = pf.bbox(self.sel_values) print("** Overall:", fmt_bbox(bb))
def draw_sel(self, clear=True, annot=None, **kargs): # print("DRAWING WITH ", kargs) # print(pf.canvas.camera.angles) pf.draw(self.selection, clear=clear, **kargs) if Annotations._active: # print("Drawing Annotations") # print(pf.canvas.camera.angles) Annotations.draw(self.selection, annot=annot) print(pf.canvas.camera.angles) def select_all(self): self.selection = self.choices def deselect_all(self): self.selection = [] def delete_sel(self): print(f"DELETE {self.selection}") self.proj.forget(*self.selection) self.selection = [] self.refresh() def keep_sel(self): self.proj.forget([k for k in self.proj.keys() if k not in self.selection]) self.refresh() def invert_sel(self): self.selection = [k for k in self.choices if k not in self.selection] # self.refresh_selection()
[docs] def rename_sel(self): """Rename selected variables""" newnames = [] for name in self.selection: res = pg.askItems([('Name', name)], caption='Rename variable') if res: newname = res['Name'] if newname != name: if newname in self.proj: pf.warning(f"An object named {newname} already exists. " f"I will not rename {name}.") newname = name else: self.proj.rename(name, newname) newnames.append(newname) self.refresh() self.selection = newnames
[docs] def edit_sel(self): """Edit a global variable.""" for name in self.selection: obj = self.proj[name] if hasattr(obj, 'edit'): # Call specialized editor obj.edit(name) elif isinstance(obj, (str, int, float)): # Use general editor res = pg.askItems([(name, obj)]) if res: print(res) self.proj.update(res)
def export_sel(self, ftype=None): ftype = 'project' compr = False cur = pf.cfg['workdir'] res = pg.askFile(cur, filter=ftype, compr=compr) if res: # convert widget data to writeGeometry parameters fn = res.pop('filename') print(f"Writing geometry file {fn}") nobj = pf.writeGeometry(fn, self.sel_dict(), compr=compr, **res) print(f"Objects written: {nobj}") ### functions for making a selection ### def accept_sel(self): if self.validate(): self.returncode = pg.Dialog.ACCEPTED self.signals.SELECTED.emit() else: self.returncode = pg.Dialog.REJECTED self.results = {} def cancel_sel(self): self.returncode = pg.Dialog.REJECTED self.results = {} self.signals.SELECTED.emit() def wait_sel(self, close=False, hide=False): self.show() loop = QtCore.QEventLoop() self.signals.SELECTED.connect(loop.quit) self.rejected.connect(loop.quit) loop.exec_() if hide: self.hide() if close: self.close() return self.results
[docs] def set_filter(self, fname=None, *, clas=None, like=None, func=None): """Set the Project filter and update Project Manager accordingly""" self.proj.set_filter(fname=fname, clas=clas, like=like, func=func) self._no_callback = True self['filter'].setChoices(pf.PF.available_filters()) self._no_callback = False self['filter'].setValue(self.proj._curfilter)
[docs] def ask_sel(self, fname=None, *, clas=None, like=None, func=None, single=False): """Ask the user to make a selection Pops up the Project Manager with the provided arguments, lets the user make a selection, and returns the selected object names. Takes all the parameters like :meth:`Project.contents`. Only the extra parameters are explained below. Parameters ---------- single: bool, optional If True, only a single object should be selected. The default allows a multiple objects selection. Returns ------- list The list of selected objects, empty if the dialog was canceled or nothing was selected. """ if fname or clas or like or func: self.set_filter(fname=fname, clas=clas, like=like, func=func) self['objects'].setSingleMode(single) self.wait_sel() self.hide() ret = self.selection if self.returncode == pg.Dialog.ACCEPTED else None self['objects'].setSingleMode(False) return ret
[docs] def check_sel(self, *, fname=None, clas=None, like=None, func=None, single=False, warn=True, ask=True): """Check the current selection. Checks that a current selection exists and conforms to the provided requirements. Takes all the parameters like :meth:`ask_sel` and the extra ones explained below. Parameters ---------- warn: bool, optional If True (default), a warning is displayed if the selection is empty or there is more than one selected object when ``single=True`` was specified. Setting to False suppresses the warning, which may be useful in batch processing. ask: bool, optional If True and the selection is not ok, calls ask_sel with the same parameters to let the user adkust the selection. Then runs the check again. Returns ------- bool True if the selection is not empty and conforms to the provided requirements. """ names = self.selection # TODO: test ALL filters !! if clas: anames = [n for n in names if isinstance(pf.PF[n], clas)] if len(anames) < len(names): self.selection = names = anames if len(names) == 0: if warn: pf.warning("No objects were selected") ok = False elif single and len(names) > 1: if warn: pf.warning("You should select exactly one object") ok = False else: ok = True if not ok and ask: self.ask_sel(fname=fname, clas=clas, like=like, func=func, single=single) ok = self.check_sel(fname=fname, clas=clas, like=like, func=func, single=single, warn=False, ask=False) return ok
[docs] def get_sel(self, **kargs): """Check the current selection and return the selected objects Parameters are like for check_sel. Returns ------- list | object If ``single=True`` was specified, a single object from the current Project. Else, a list of such objects. The returned list is actually a :class:`~pyformex.olist.List` instance, so it can directly accept transformations. If nothing was selected, an empty list is returned. """ if self.check_sel(**kargs): if kargs.get('single', False): return self.sel_values[0] else: return pf.List(self.sel_values) else: return []
[docs] def set_sel(self, values, names=None, suffix=None, fname=None): """Set a new selection specifying names and values, and redraw. Parameters ---------- values: list List of values. names: list, optional List of object names, same length as values. If not provided, the current selection is used. suffix: str String to append to all names. fname: str Filter name to set on the Project. """ if names is None: names = self.selection if suffix: names = [name + suffix for name in names] self.proj.update2(names, values) if fname: self.set_filter(fname) print("SET SELECTION", names) self.selection = names print("DRAW SELECTION") self.draw_sel()
[docs] def remember_sel(self): """Remember the selection""" self.proj.remember(self.selection)
# TODO: could be merged into set_sel ??
[docs] def replace_sel(self, values, remember=False): """Replace the current values of selection by new ones and redraw. Parameters ---------- values: list The new objects to store under the names of the current selection. remember: bool If True, the old values are remembered (to enable undo operation), and drawn in yellow. """ names = self.selection if len(names) != len(values): raise ValueError("values should have same length as selection") if remember: self.proj.remember(names) self.proj.update2(names, values) self.draw_sel() if remember: pg.draw(self.proj._saved, color='yellow', bbox=None, clear=False, wait=False)
[docs] def readGeometry(self, files, target=None, select=True, draw=True, **kargs): """Read a number of Geometry files, select and draw the results""" objects = {} if not isinstance(files, (list, tuple)): files = (files, ) for path in files: print(f"Reading geometry file {path}") with pg.busyCursor(): obj = pf.readGeometry(filename=path, **kargs, target=target) pf.PF.update(obj) print("Items read: " + ', '.join([ f"{k}: {obj[k].__class__.__name__}" for k in obj])) objects.update(obj) # print(list(all_obj.keys())) if select: self.set_filter('Geometry') self.selection = [k for k in obj.keys() if not k.startswith('_')] if draw: self.draw_sel() return objects
pf.PM = None
[docs]def projectmanager(show=None): """Create/show/hide the Project Manager. The Project Manager is a persistent object in this module. If it already exists, it is reused. Else, a new one is created and set. It can be hidden or shown. Parameters ---------- show: bool, optional If True, shows the Project Manager. If False, hides it. If not specified, leaves it as it was (which is shown for a newly created Project Manager is always shown). Returns ------- ProjectManager The current Project Manager. """ if pf.PM is None: pf.PM = ProjectManager(pf.PF) if show: pf.PM.show() elif show is False: pf.PM.hide() return pf.PM
# End