diff --git a/envisage/ui/single_project/DESIGN.txt b/envisage/ui/single_project/DESIGN.txt deleted file mode 100644 index 12062662c..000000000 --- a/envisage/ui/single_project/DESIGN.txt +++ /dev/null @@ -1,47 +0,0 @@ -These are some design notes for the E3 version of the single_project plugin, -including a rough background adapated from the README of the E2 single_project -plugin, and plans for implementing various aspects of the new plugin. -*Note: This is very much a WIP and will probably be updated/modified heavily -in a short period of time. - - -BACKGROUND: ------------ -This plugin is designed to work with a single project at a time and build -up 'state' within that project until the user explicitly saves the project. - -The plugin contributes two services to an Envisage application -- one for the -model and one for the UI. Additionally, the plugin contributes a 'Project' -perspective, 'Project' view, and a number of actions to the UI so that the -user can create, open, save, save as, and close projects. Finally, it should -be noted that the plugin manages an indicator applied to the application -window(s) titlebar as a representation that the current project has unsaved -modifications. - -The current project is visualized by a TreeEditor by default, but the user can -contribute additional Views. By contributing your own Project factory, the -user can define your own custom projects. - - -E2 single_projects vs. E3 single_projects: ------------------------------------------ -As described by Martin Chilvers: - "domain-objects" are at the highest level of abstraction in your design and -hence are the objects that are understood by the user (e.g. Book, Person, Log, -LogSuite, Well etc). - -The original single_project plugin used resources and resource types to adapt -domain-objects to add certain behaviours to them that various Envisage plugins -expected. However, this approach restricted the addition of new behaviours to -resource types, as well as added a lot of cruft to the process of registering -new resources/resource types. - -The E3 single_project plugin uses Traits Adapters and takes advantage of the new -ITreeNode interface. The new TreeEditor has been extended to support handling -unknown domain-objects by adapting them to the ITreeNode interface. An -ITreeNodeAdapter needs to be defined for any object that you want to be -displayed in the TreeEditor, such as your Project class and it's sub-nodes. -The Adapter exposes certain key ITreeNode interface methods, such as -get_children, when_children_changed, get_lable, etc... This eliminates the -extra cruft that was necessary in the E2 single_project plugin, such as the -node monitor, node resource type, etc... diff --git a/envisage/ui/single_project/README.txt b/envisage/ui/single_project/README.txt deleted file mode 100644 index 423428119..000000000 --- a/envisage/ui/single_project/README.txt +++ /dev/null @@ -1,128 +0,0 @@ -Last updated: 2006.02.05 - - -BACKGROUND: ------------ -This plugin is designed to work with a single project at a time and build -up 'state' within that project until the user explicitly saves the project. - -The plugin contributes two services to an Envisage application -- one for the -model and one for the UI. Additionally, the plugin contributes a 'Project' -perspective, 'Project' view, and a number of actions to the UI so that the -user can create, open, save, save as, and close projects. Finally, it should -be noted that the plugin manages an indicator applied to the application -window(s) titlebar as a representation that the current project has unsaved -modifications. - -The current project is visualized using a ResourceTree control and thus -supports all the visualization and interaction customization of resources. -This also makes makes it easy to build a hierarchy of nodes within the tree -by implementing the apptools.naming.Context interface. - -Finally, this plugin contributes a single extension point which provides the -capability to define your own custom projects. - - -PRE-REQUISITES: ---------------- -This plugin requires the use of the envisage framework and the -following plugins: - - envisage.core - - envisage.resource - - envisage.workbench - - envisage.plugins.python_shell - - -HOW TO USE: ------------ -This plugin provides a base class for projects but, as of now, that class -has no capability to contain any data. So for this plugin to be useful within -an Envisage application you have to complete the following: - -- Contribute a project_plugin_definition.FactoryDefinition to the Envisage - application. This definition should point to a class derived from - project_factory.ProjectFactory which will be used to create new projects - as well as handle opening of persisted projects. To do that, your - ProjectFactory should override the 'create' and 'open' methods as - appropriate to return an instance of a class derived from project.Project. - Note that multiple plugins can contribute FactoryDefinitions but only the - first contributed definition with the highest priority will be used. - -- You will need to derive a class from project.Project which will be used to - contain your project's data. This class should: - - override the trait_view trait to provide the UI content that will be - shown by the plugin (within a wizard dialog) to allow a user to - initialize a newly created project. - - override the _contains_resource() method so that the plugin can close - editors for resources in your project when your project closes. - - set the 'dirty' trait to True anytime modifications are made to your - project data - - optionally override the Project.start() and Project.stop() methods - to handle any work needing to be done when a project becomes the - current project or is no longer the current project. For example, - the base Project uses the stop() method to close any editors associated - with resources contained in the project. - - optionally override the _load_hook() and _save() methods if you wish - to use the base class's infrastructure for loading and saving projects - but have additional work that needs to be accomplished during those - events. Note that the plugin's UI service calls the save() method and - the ProjectFactory calls the load() method. - - optionally override the 'load()' and 'save()' methods if you want to - completely replace the base class's infrastructure for loading and - saving projects. Note that the plugin's UI service calls the save() - method and the ProjectFactory calls the load() method. - -- You will likely want to register resource types -- and associated classes - such as node types, monitors, and resource references -- for the resources - that can be contained within your project. This will allow you to take - maximum advantage of the infrastructure of the Project view's ResourceTree - control. If you do this, you should note that this plugin registers a - ProjectResourceType, pointing to a ProjectNodeType, that is used to - visualize the root project node. You don't need to derive from these unless - you want to customize the resource definition of the project node. - -- Another option for implementing the visualization of project contents would - be to derive from ProjectView and override the _create_project_control() - and _update_project_control() methods to replace the ResourceTree control - with some other UI control. If you do the later, you will likely need to - create your own project plugin definition but you should be able to reuse - almost all of the infrastructure classes contained in this plugin. - -- If the resources within your project can be edited in Editors, you should - derive Editor classes from ProjectEditor so that the project plugin - framework can close these editors when the project closes. - - -KNOWN ISSUES: -------------- -- Due to the current capabilities of the workbench plugin, this plugin can't - do the ideal closing hook whereby, if the current project is modified, the - user gets prompted to save it, close it, or cancel the closing of the window. - The best we can do is follow the prompt to close the workbench window with a - dialog querying the user whether they want to save the current project or - not. There is an Enthought Trac ticket to add the necessary capability to - the workbench plugin and this plugin will be 'fixed' shortly thereafter. - -- The Project class doesn't support any sort of versioning framework in its - saving and loading. This will cause problems as the content and - capabilities of projects evolve throughout the software development cycle - of an application that uses this plugin. - - -TO-DO: ------- -- It would be nice if the Project class was modified such that it could - actually be used without needing to be derived from. One possibility for - this would be to make it a full-fledged folder type Context in and of - itself including support for binding and unbinding. If this was done, it - should be easy enough to re-use resource types, node types, etc. defined - for other Envisage plugins within projects. - -- It might be nice if editors that were open when a project was saved were - restored when the project was re-loaded. One mechanism to do this would - be to modify the Project._editors trait to contain ResourceReferences rather - than Resources as the keys. Then when pickling, we would simple dump the - editor references and persist just these keys. Upon loading, iterate through - the keys/resource references and open an editor for each. One question about - this mechanism is that currently we don't track order or position of editors - so that info wouldn't be recovered. diff --git a/envisage/ui/single_project/TODO.txt b/envisage/ui/single_project/TODO.txt deleted file mode 100644 index 6b4457c65..000000000 --- a/envisage/ui/single_project/TODO.txt +++ /dev/null @@ -1,9 +0,0 @@ -To-Do List: - - * Update README and other documentation/examples. - * Implement a way to publish the ITreeNodeAdapters so that they don't have to - be in the same file as the class which they adapt. - * Improve the way we are binding dynamic objects and how they are being - retrieved. - * Improve the handling of children in the adapters to remove redudancy. - * Lots of cleanup left. diff --git a/envisage/ui/single_project/__init__.py b/envisage/ui/single_project/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/envisage/ui/single_project/action/__init__.py b/envisage/ui/single_project/action/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/envisage/ui/single_project/action/api.py b/envisage/ui/single_project/action/api.py deleted file mode 100644 index 328c571fa..000000000 --- a/envisage/ui/single_project/action/api.py +++ /dev/null @@ -1,14 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -from .new_project_action import NewProjectAction -from .open_project_action import OpenProjectAction -from .save_project_action import SaveProjectAction -from .save_as_project_action import SaveAsProjectAction -from .close_project_action import CloseProjectAction -from .switch_to_action import SwitchToAction diff --git a/envisage/ui/single_project/action/close_project_action.py b/envisage/ui/single_project/action/close_project_action.py deleted file mode 100644 index 32531910b..000000000 --- a/envisage/ui/single_project/action/close_project_action.py +++ /dev/null @@ -1,43 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" An action to close the current project. This is only enabled when - there is a current project. -""" - -# Enthought library imports -from envisage.ui.single_project.project_action import ProjectAction -from pyface.api import ImageResource - -############################################################################## -# class 'CloseProjectAction' -############################################################################## - - -class CloseProjectAction(ProjectAction): - """ An action to close the current project. This is only enabled when - there is a current project. - """ - - # The universal object locator (UOL). - uol = "envisage.ui.single_project.ui_service.UiService" - - # The name of the method to invoke on the object. - method_name = "close" - - # A longer description of the action. - description = "Close the current project" - - # The action's image (displayed on tool bar tools etc). - image = ImageResource("close_project") - - # The action's name (displayed on menus/tool bar tools etc). - name = "Close" - - # A short description of the action used for tooltip text etc. - tooltip = "Close this project" diff --git a/envisage/ui/single_project/action/configure_action.py b/envisage/ui/single_project/action/configure_action.py deleted file mode 100644 index 4acb12352..000000000 --- a/envisage/ui/single_project/action/configure_action.py +++ /dev/null @@ -1,41 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" An action that configures an item in the project tree. """ - -# Enthought library imports. -from envisage.ui.single_project.project_action import ProjectAction - -############################################################################## -# class 'ConfigureAction' -############################################################################## - - -class ConfigureAction(ProjectAction): - """ An action that configures an item in the project tree. - - 'Configures' in this sense means pop up a trait sheet! - - """ - - ########################################################################### - # 'Action' interface. - ########################################################################### - - def perform(self, event): - """ Performs the action. """ - - # fixme: We would like to use "kind='modal'" here, but it doesn't - # work! The bug we see is that when the dialog is complete the - # viscosity model assigned to the trait is NOT the same as the - # viscosity model in the material's '_dirty_objects' collection! - event.node.obj.edit_traits( - parent=event.window.control, kind="livemodal" - ) - - return diff --git a/envisage/ui/single_project/action/images/close_project.png b/envisage/ui/single_project/action/images/close_project.png deleted file mode 100644 index 302fb33a0..000000000 Binary files a/envisage/ui/single_project/action/images/close_project.png and /dev/null differ diff --git a/envisage/ui/single_project/action/images/new_project.png b/envisage/ui/single_project/action/images/new_project.png deleted file mode 100644 index 19a16cfa9..000000000 Binary files a/envisage/ui/single_project/action/images/new_project.png and /dev/null differ diff --git a/envisage/ui/single_project/action/images/open_project.png b/envisage/ui/single_project/action/images/open_project.png deleted file mode 100644 index e06d13c46..000000000 Binary files a/envisage/ui/single_project/action/images/open_project.png and /dev/null differ diff --git a/envisage/ui/single_project/action/images/save_as_project.png b/envisage/ui/single_project/action/images/save_as_project.png deleted file mode 100644 index baabb7aae..000000000 Binary files a/envisage/ui/single_project/action/images/save_as_project.png and /dev/null differ diff --git a/envisage/ui/single_project/action/images/save_project.png b/envisage/ui/single_project/action/images/save_project.png deleted file mode 100644 index b30e7ff00..000000000 Binary files a/envisage/ui/single_project/action/images/save_project.png and /dev/null differ diff --git a/envisage/ui/single_project/action/images/switch_project.png b/envisage/ui/single_project/action/images/switch_project.png deleted file mode 100644 index c5224b49c..000000000 Binary files a/envisage/ui/single_project/action/images/switch_project.png and /dev/null differ diff --git a/envisage/ui/single_project/action/new_project_action.py b/envisage/ui/single_project/action/new_project_action.py deleted file mode 100644 index a9cc4a692..000000000 --- a/envisage/ui/single_project/action/new_project_action.py +++ /dev/null @@ -1,39 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" An action that creates a new project. """ - -# Enthought library imports. -from envisage.ui.single_project.project_action import ProjectAction -from pyface.api import ImageResource - -############################################################################## -# class 'NewProjectAction' -############################################################################## - - -class NewProjectAction(ProjectAction): - """ An action that creates a new project. """ - - # The universal object locator (UOL). - uol = "envisage.ui.single_project.ui_service.UiService" - - # The name of the method to invoke on the object. - method_name = "create" - - # A longer description of the action. - description = "Create a project" - - # The action's image (displayed on tool bar tools etc). - image = ImageResource("new_project") - - # The action's name (displayed on menus/tool bar tools etc). - name = "New..." - - # A short description of the action used for tooltip text etc. - tooltip = "Create a project" diff --git a/envisage/ui/single_project/action/open_project_action.py b/envisage/ui/single_project/action/open_project_action.py deleted file mode 100644 index 0e2a956b2..000000000 --- a/envisage/ui/single_project/action/open_project_action.py +++ /dev/null @@ -1,39 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" An action that opens a project. """ - -# Enthought library imports. -from envisage.ui.single_project.project_action import ProjectAction -from pyface.api import ImageResource - -############################################################################## -# class 'OpenProjectAction' -############################################################################## - - -class OpenProjectAction(ProjectAction): - """ An action that opens a project. """ - - # The universal object locator (UOL). - uol = "envisage.ui.single_project.ui_service.UiService" - - # The name of the method to invoke on the object. - method_name = "open" - - # A longer description of the action. - description = "Open an existing project" - - # The action's image (displayed on tool bar tools etc). - image = ImageResource("open_project") - - # The action's name (displayed on menus/tool bar tools etc). - name = "Open..." - - # A short description of the action used for tooltip text etc. - tooltip = "Open a project" diff --git a/envisage/ui/single_project/action/rename_action.py b/envisage/ui/single_project/action/rename_action.py deleted file mode 100644 index d0398b99b..000000000 --- a/envisage/ui/single_project/action/rename_action.py +++ /dev/null @@ -1,72 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" An action that renames an item in the project tree. """ - - -# Enthought library imports. -from envisage.ui.single_project.project_action import ProjectAction - - -class RenameAction(ProjectAction): - """ Renames an item in the project tree. """ - - #### 'Action' interface ################################################### - - # The action's name (displayed on menus/tool bar tools etc). - name = "Rename" - - ########################################################################### - # 'Action' interface. - ########################################################################### - - def perform(self, event): - """ Performs the action. """ - - event.widget.edit_label(event.node) - - return - - # fixme: This should be a selection listener action that probably is only - # enabled if there is exactly ONE item in the selection and it is editable. - - -## ########################################################################### -## # 'Action' interface. -## ########################################################################### - -## def refresh(self): -## """ Refresh the enabled/disabled state of the action etc. - -## This is called whenever the workbench window that the action is in -## and/or the selection in that window have been changed. - -## """ - -## resource_manager = self.window.application.get_service( -## 'envisage.resource.IResourceManager' -## ) - -## # fixme: It seems there is a glitch in the tree selection handling. -## # When the selection changes we get an empty selection first, then -## # the new selection. -## if len(self.window.selection) > 0: -## for node in self.window.selection: -## resource_type = resource_manager.get_type_of(node.obj) -## if resource_type is None or resource_type.node_type is None: -## self.enabled = False -## break - -## if not resource_type.node_type.is_editable(node.obj): -## self.enabled = False -## break - -## else: -## self.enabled = True - -## return diff --git a/envisage/ui/single_project/action/save_as_project_action.py b/envisage/ui/single_project/action/save_as_project_action.py deleted file mode 100644 index 97058ecab..000000000 --- a/envisage/ui/single_project/action/save_as_project_action.py +++ /dev/null @@ -1,127 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" An action that saves the current project to a different location. """ - -# Enthought library imports. -from envisage.ui.single_project.project_action import ProjectAction -from pyface.api import ImageResource - -############################################################################## -# class 'SaveAsProjectAction' -############################################################################## - - -class SaveAsProjectAction(ProjectAction): - """ An action that saves the current project to a - different location. - """ - - # The universal object locator (UOL). - uol = "envisage.ui.single_project.ui_service.UiService" - - # The name of the method to invoke on the object. - method_name = "save_as" - - # A longer description of the action. - description = "Save the current project to a different location" - - # The action's image (displayed on tool bar tools etc). - image = ImageResource("save_as_project") - - # The action's name (displayed on menus/tool bar tools etc). - name = "Save As..." - - # A short description of the action used for tooltip text etc. - tooltip = "Save this project to a different location" - - #### public interface #################################################### - - def refresh(self): - """ - Refresh the enabled state of this action. - - This implementation enables the action when there is a current project - which is marked as being allowed to do a 'save as' operation. - - """ - - self.enabled = ( - self._refresh_project_exists() - and self._refresh_is_save_as_allowed() - ) - - return - - #### trait handlers ###################################################### - - def _on_project_changed(self, obj, trait_name, old, new): - """ - Handle changes to the value of the current project. - - Extended to ensure that we listen for changes to the is_save_as_allowed - flag on the current project. - - """ - - if old is not None: - self._update_project_listeners(old, remove=True) - if new is not None: - self._update_project_listeners(new, remove=False) - - super()._on_project_changed( - obj, trait_name, old, new - ) - - ########################################################################## - # 'SaveAsAction' interface - ########################################################################## - - #### protected interface ################################################# - - def _refresh_is_save_as_allowed(self): - """ - Return the refresh state according to whether the current project is - marked as being capable of doing a 'save as'. - - Returns True if the action should be enabled and False otherwise. - - """ - - return self.model_service.project.is_save_as_allowed - - def _update_project_listeners(self, project, remove): - """ - Update listeners on the specified project. - - """ - - logger.debug( - (remove and "Removing " or "Adding ") - + "listeners on project [%s] for SaveAsAction [%s]", - project, - self, - ) - - project.on_trait_change( - self._on_is_save_as_allowed, "is_save_as_allowed", remove=remove - ) - - return - - #### trait handlers ###################################################### - - def _on_is_save_as_allowed(self, obj, trait_name, old, new): - """ - Handle changes to the value of the current project's is_save_as_allowed. - - """ - - self.refresh() - - return diff --git a/envisage/ui/single_project/action/save_project_action.py b/envisage/ui/single_project/action/save_project_action.py deleted file mode 100644 index 1f540d044..000000000 --- a/envisage/ui/single_project/action/save_project_action.py +++ /dev/null @@ -1,122 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" An action that saves the current project. """ - -# Enthought library imports. -from envisage.ui.single_project.project_action import ProjectAction -from pyface.api import ImageResource - -############################################################################## -# class 'SaveProjectAction' -############################################################################## - - -class SaveProjectAction(ProjectAction): - """ An action that saves the current project. """ - - # The universal object locator (UOL). - uol = "envisage.ui.single_project.ui_service.UiService" - - # The name of the method to invoke on the object. - method_name = "save" - - # A longer description of the action. - description = "Save the current project" - - # The action's image (displayed on tool bar tools etc). - image = ImageResource("save_project") - - # The action's name (displayed on menus/tool bar tools etc). - name = "Save" - - # A short description of the action used for tooltip text etc. - tooltip = "Save this project" - - #### public interface #################################################### - - def refresh(self): - """ - Refresh the enabled state of this action. - - This implementation enables the action when there is a current project - which is marked as saveable. - - """ - - self.enabled = ( - self._refresh_project_exists() and self._refresh_is_save_allowed() - ) - - return - - #### trait handlers ###################################################### - - def _on_project_changed(self, obj, trait_name, old, new): - """ - Handle changes to the value of the current project. - - Extended to ensure that we listen for changes to the saveable flag on - the current project. - - """ - - if old is not None: - self._update_project_listeners(old, remove=True) - if new is not None: - self._update_project_listeners(new, remove=False) - - super()._on_project_changed(obj, trait_name, old, new) - - ########################################################################## - # 'SaveAction' interface - ########################################################################## - - #### protected interface ################################################# - - def _refresh_is_save_allowed(self): - """ - Return the refresh state according to whether the current project is - marked as saveable. - - Returns True if the action should be enabled and False otherwise. - - """ - - return self.model_service.project.is_save_allowed - - def _update_project_listeners(self, project, remove): - """ - Update listeners on the specified project. - - """ - - logger.debug( - (remove and "Removing " or "Adding ") - + "listeners on project [%s] for SaveAction [%s]", - project, - self, - ) - - project.on_trait_change( - self._on_is_save_allowed, "is_save_allowed", remove=remove - ) - - return - - #### trait handlers ###################################################### - - def _on_is_save_allowed(self, obj, trait_name, old, new): - """ - Handle changes to the value of the current project's is_save_allowed. - - """ - - self.refresh() - - return diff --git a/envisage/ui/single_project/action/switch_to_action.py b/envisage/ui/single_project/action/switch_to_action.py deleted file mode 100644 index 13c08709f..000000000 --- a/envisage/ui/single_project/action/switch_to_action.py +++ /dev/null @@ -1,36 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" An action that switches the project perspective. """ - -# Enthought library imports. -from envisage.ui.single_project.project_action import ProjectAction -from pyface.api import ImageResource - - -class SwitchToAction(ProjectAction): - """ An action that switches the project perspective. """ - - # A longer description of the action. - description = "View the current project in the Project perspective" - - # The action's image (displayed on tool bar tools etc). - image = ImageResource("switch_project") - - # The action's name (displayed on menus/tool bar tools etc). - name = "Switch To Project" - - # A short description of the action used for tooltip text etc. - tooltip = "Go to the Project perspective" - - def perform(self, event): - """ Perform the action. """ - - self.window.application.about() - - return diff --git a/envisage/ui/single_project/api.py b/envisage/ui/single_project/api.py deleted file mode 100644 index ff4cc1aa4..000000000 --- a/envisage/ui/single_project/api.py +++ /dev/null @@ -1,21 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -# IDs of services provided by this plugin -from .services import IPROJECT_MODEL, IPROJECT_UI - -# Commonly referred to classes within this plugin -from .factory_definition import FactoryDefinition -from .model_service import ModelService -from .project import Project -from .project_action import ProjectAction -from .project_factory import ProjectFactory -from .view.project_view import ProjectView - -# FIXME: Add back this import when it actually works :) -# from .editor.project_editor import ProjectEditor diff --git a/envisage/ui/single_project/default_path_preference_page.py b/envisage/ui/single_project/default_path_preference_page.py deleted file mode 100644 index a300a66a0..000000000 --- a/envisage/ui/single_project/default_path_preference_page.py +++ /dev/null @@ -1,47 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" Preference page for default path for a project -""" - -# Enthought library imports -from apptools.preferences.ui.api import PreferencesPage -from traits.api import Directory, Str -from traitsui.api import View, Item - -# Global assignment of ID -ID = "envisage.ui.single_project" - -# ------------------------------------------------------------------------------- -# DefaultPathPreferencePage Class -# ------------------------------------------------------------------------------- - - -class DefaultPathPreferencePage(PreferencesPage): - """ Preference page for default path for a plugin. - """ - - # The page name (this is what is shown in the preferences dialog. - name = "Single Project" - - # The path to the preferences node that contains the preferences. - preferences_path = "envisage.ui.single_project" - - #### Preferences ########################################################## - - # Choose the unit system that needs to be used for the project - preferred_path = Directory("") - - # Set the traits view - traits_view = View( - Item( - "preferred_path", - style="custom", - tooltip="Path that will be used for storing projects", - ) - ) diff --git a/envisage/ui/single_project/editor/__init__.py b/envisage/ui/single_project/editor/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/envisage/ui/single_project/editor/project_editor.py b/envisage/ui/single_project/editor/project_editor.py deleted file mode 100644 index 423cab51a..000000000 --- a/envisage/ui/single_project/editor/project_editor.py +++ /dev/null @@ -1,93 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" -A base class for editors that can be tracked by single project plugin projects. - -""" - -# Standard library imports. -import logging - -# Enthought library imports -from envisage.workbench import DecoratedEditor -from traits.api import Instance - -# Application specific imports. -from envisage.single_project.services import IPROJECT_MODEL - - -# Setup a logger for this module. -logger = logging.getLogger(__name__) - - -class ProjectEditor(DecoratedEditor): - """ - A base class for editors that can be tracked by single project plugin - projects. - - """ - - ######################################################################### - # Attributes - ######################################################################### - - ### public 'ProjectEditor' interface #################################### - - # The project containing the resource we're editing - project = Instance("envisage.single_project.project.Project") - - ######################################################################### - # `object` interface - ######################################################################### - - #### operator methods ################################################### - - def __init__(self, **traits): - """ - Constructor. - - Extended to associate ourself with the current project. - - """ - - super().__init__(**traits) - - # Make sure the current project knows this editor is associated with - # it's resources - model_service = self.window.application.get_service(IPROJECT_MODEL) - self.project = model_service.project - self.project.register_editor(self.resource, self) - - return - - ######################################################################### - # 'Editor' interface. - ######################################################################### - - ### public 'Editor' interface ########################################### - - def destroy_control(self): - """ - Destroys the toolkit-specific control that represents the editor. - - Extended to ensure that the current project stops associating us - with its resources. - - """ - - # Only do something if the editor is still open - if self.control: - logger.debug("Destroying control in ProjectEditor [%s]", self) - - # Unregister from the associated project immediately. - self.project.register_editor(self.resource, self, remove=True) - - super().destroy_control() - - return diff --git a/envisage/ui/single_project/factory_definition.py b/envisage/ui/single_project/factory_definition.py deleted file mode 100644 index eef3e2f01..000000000 --- a/envisage/ui/single_project/factory_definition.py +++ /dev/null @@ -1,29 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -# Enthought imports -from traits.api import HasTraits, Int, Str - - -class FactoryDefinition(HasTraits): - """ - A project factory definition. - - An instance of the specified class is used to open and/or create new - projects. - - The extension with the highest priority wins! In the event of a tie, - the first instance wins. - - """ - - # The name of the class that implements the factory. - class_name = Str - - # The priority of this factory - priority = Int diff --git a/envisage/ui/single_project/model_service.py b/envisage/ui/single_project/model_service.py deleted file mode 100644 index 384634515..000000000 --- a/envisage/ui/single_project/model_service.py +++ /dev/null @@ -1,163 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" -The Envisage service providing the model state for the single -project plugin. - -""" - -# Standard imports -import logging -import os -import shutil - -# Enthought library imports -from envisage.api import IApplication -from apptools.preferences.api import IPreferences -from traits.api import Any, HasTraits, Instance, List - - -# Setup a logger for this module. -logger = logging.getLogger(__name__) - - -class ModelService(HasTraits): - """ - The Envisage service providing the model state for the single - project plugin. - - """ - - ########################################################################## - # Attributes (Traits) - ########################################################################## - - ### public 'ModelService' interface ###################################### - - # The Envisage application that this service is part of. - application = Instance(IApplication) - - # The factory to use for creating new projects - factory = Instance( - "envisage.ui.single_project.project_factory." "ProjectFactory" - ) - - # The preferences to be exposed through this service. - preferences = Instance(IPreferences) - - # The currently open project - project = Instance("envisage.ui.single_project.project.Project") - - # The current selection within the current project. - selection = List(Any) - - ########################################################################## - # 'object' interface. - ########################################################################## - - ### operator methods ##################################################### - - def __init__(self, application, factory, **traits): - """ - Constructor. - - We require a reference to an Envisage application and a project - factory to create an instance. - - """ - - super().__init__( - application=application, factory=factory, **traits - ) - - return - - ########################################################################## - # 'ModelService' interface. - ########################################################################## - - ### public interface ##################################################### - - def are_projects_files(self): - """ - Returns True if project instances are saved as files and False if - they are saved as directories. - - """ - - return self.factory.PROJECT_CLASS.PROJECTS_ARE_FILES - - def clean_location(self, location): - """ - Ensures that there are no existing files or directories at the - specified location by removing them. Exceptions are raised if - there are any errors cleaning out existing files or directories. - - """ - - logger.debug("Trying to clean location [%s]", location) - - if os.path.isfile(location): - os.path.remove(location) - else: - shutil.rmtree(location) - - return - - def get_default_path(self): - """ - Return the default location for projects. - - """ - - return self.factory.PROJECT_CLASS.get_default_path(self.application) - - ### trait handlers ####################################################### - - def _project_changed(self, old, new): - """ - Called whenever the current project is changed. - - We hook this to make sure the new project knows it's current and - the old project knows it's not. - - """ - - logger.debug( - "Detected project change from [%s] to [%s] in " - "ModelService [%s]", - old, - new, - self, - ) - - if old is not None: - old.stop() - self.selection = [] - if new is not None: - new.start() - - return - - def _selection_changed(self, old, new): - """ - Called whenever the selection within the project is changed. - - Implemented simply to log the change. - - """ - - logger.debug( - "ModelService [%s] selection changed from [%s] to [%s] ", - self, - old, - new, - ) - - return diff --git a/envisage/ui/single_project/preferences.ini b/envisage/ui/single_project/preferences.ini deleted file mode 100644 index dded470f2..000000000 --- a/envisage/ui/single_project/preferences.ini +++ /dev/null @@ -1,2 +0,0 @@ -[enthought.envisage.ui.single_project] -preferred_path = '' \ No newline at end of file diff --git a/envisage/ui/single_project/project.py b/envisage/ui/single_project/project.py deleted file mode 100644 index 5e70cdd0c..000000000 --- a/envisage/ui/single_project/project.py +++ /dev/null @@ -1,762 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" -A base class for projects that can be displayed by the single_project -plugin. - -""" - -# Standard library imports. -import logging -import os -import re -import unicodedata - -# Enthought library imports -from traits.etsconfig.api import ETSConfig -import apptools.sweet_pickle -from apptools.io.api import File -from traits.api import ( - Any, - Bool, - Dict, - Directory, - HasTraits, - Instance, - Property, - Str, -) -from traitsui.api import Group, View - -# Local imports. -# from envisage.ui.single_project.editor.project_editor import \ -# ProjectEditor -from envisage.api import Application - - -# Setup a logger for this module. -logger = logging.getLogger(__name__) - - -class Project(HasTraits): - """ - A base class for projects that can be displayed by the single_project - plugin. - - """ - - ########################################################################## - # CLASS Attributes - ########################################################################## - - #### public 'Project' class interface #################################### - - # Indicates whether instances of this project class are stored as files or - # directories. The rest of the single_project plugin will follow this - # setting when using this project class. - # - # This is meant to be a constant for the lifetime of this class! - PROJECTS_ARE_FILES = True - - # Current envisage application. - application = Instance(Application, transient=True) - - #### protected 'Project' class interface ################################# - - # Format used to create a unique name from a location and a counter. - _unique_name_format = "%s_%s" - - ########################################################################## - # Attributes - ########################################################################## - - #### public 'Project' interface ########################################## - - # True if this project contains un-saved state (has been modified.) - dirty = Bool(False, transient=True) - - # True if we allow save requests on the project. - is_save_allowed = Bool(True, transient=True) - - # True if we allow save_as requests on the project. - is_save_as_allowed = Bool(True, transient=True) - - # The location of this project on the filesystem. The default value - # depends on the runtime environment so we use a traits default method to - # set it. - location = Directory - - # The name of this project. This is calculated from the project's current - # location or, if there is no location, from a default value. See the - # property's getter method. - name = Property(Str) - - # The UI view to use when creating a new project - traits_view = View( - Group("location"), - title="New Project", - id="envisage.single_project.project.Project", - buttons=["OK", "Cancel"], - width=0.33, - # Ensure closing via the dialog close button is the same - # as clicking cancel. - close_result=False, - # Ensure the user can resize the dialog. - resizable=True, - ) - - #### protected 'Project' interface ####################################### - - # A list of editors currently open to visualize our resources - # FIXME: Re-add the ProjectEditor's(if we need them) once they are fixed. - # _editors = Dict(Any, ProjectEditor, transient=True) - - # The cache of this project's name. We can't just initialize it to a - # default value since the location may be cleared at any time. - _name = Str(transient=True) - - ########################################################################## - # 'Object' interface. - ########################################################################## - - #### operator methods #################################################### - - def __getstate__(self): - """ - Get the state of this object for pickling. - - Extended to limit which attributes get pickled and also to add - version numbers to our pickle. - - It is STRONGLY recommended that derived classes not leverage the - pickling mechanism if they wish to store additional data in OTHER - files as part of their project. Instead, they should override the - *_save* method. - - Extending this method in derived classes IS appropriate if you want - to store additional data in the SAME pickle file as the project. - - """ - - # Determine the starting point for our state. - state = super().__getstate__().copy() - - # Remove any transient traits. - for trait_name in self.trait_names(transient=True): - state.pop(trait_name, None) - - # Add in our current version number. Note use a different attribute - # name from any base or derived class so that our numbers don't - # override theirs. - state["_project_version_major"] = 1 - state["_project_version_minor"] = 0 - - return state - - def __setstate__(self, state): - """ - Restore the state of this object during unpickling. - - Extended to upgrade pickles to the current Project version. - - It is STRONGLY recommended that derived classes not leverage the - unpickling mechanism if they wish to load data from additional pickle - files. Instead, they should override the *_load* method. - - Extending this method in derived classes IS appropriate if you want - to load additional data from the SAME pickle file as the project. - - """ - - # Get the version info out of the state dictionary. - major = state.pop("_project_version_major", 0) - state.pop("_project_version_minor", 0) - - # Upgrade to version 1. - if major < 1: - - # Remove any old attributes. - # - name is now a calculated property instead of stored value. - for key in ["name"]: - state.pop(key, None) - - # Restore our state. - return super().__setstate__(state) - - def __str__(self): - """ - Return the unofficial string representation of this object. - - """ - - result = "%s(name=%s)" % (super().__str__(), self.name) - return result - - ########################################################################## - # 'Project' interface. - ########################################################################## - - #### public interface #################################################### - - def get_default_project_location(self, application): - """ - Return the default location for a new project. - - """ - - path = self.get_default_path(application) - name = clean_filename(self.get_default_name()) - location = os.path.join(path, name) - location = self._make_location_unique(location) - - return location - - def get_default_path(cls, application): - """ - Return the default path to the parent directory for a new project. - - """ - - # When unpickling, we don't have a reference to the current application, so we - # fallback on application_home. - if application is None: - return ETSConfig.application_home - - app_preferences = application.preferences - path_id = "envisage.ui." "single_project.preferred_path" - path = app_preferences.get(path_id) - - # If the 'preferred_path' variable isn't set in the user's preferences, - # then we set to the the application home by default. - if len(path) == 0: - app_home = ETSConfig.application_home - app_preferences.set(path_id, app_home) - return app_home - - return path - - get_default_path = classmethod(get_default_path) - - def get_default_name(self): - """ - Return the default name for a new project. - - """ - - return "New Project" - - def get_pickle_filename(cls, location): - """ - Generate the project's pickle filename given a source location. - - By default, the filename IS the specified location or, when saving - projects as directories, a file called 'project' within a directory - that is the specified location, - - Derived classes may wish to use the location as the basis for - identifying the real pickle file. - - """ - - if cls.PROJECTS_ARE_FILES: - result = location - else: - result = os.path.join(location, "project") - - return result - - get_pickle_filename = classmethod(get_pickle_filename) - - def get_pickle_package(cls): - """ - Returns the pickle package to use for pickling and unpickling - projects. - - Implementors can override this to customize the way in which - projects are pickled and unpickled. - - This implementation returns the apptools.sweet_pickle package which - supports versioning and refactoring of classes. - - """ - - return apptools.sweet_pickle - - get_pickle_package = classmethod(get_pickle_package) - - def load(cls, location, application): - """ - Load a project from a specified location. - - The loaded project's location is always set to the location the project - was actually loaded from. Additionally, the dirty flag is cleared - on the loaded project. - - An exception will be raised to indicate a failure. - - """ - - # Load the project in a manner that derived classes can modify. - project = cls._load(location) - - # Ensure the project's location reflects where the project was loaded - # from and that the dirty flag is not set. - project.location = location - project.dirty = False - - # Set the project's 'application' to the running application passed in. - project.application = application - - return project - - load = classmethod(load) - - def register_editor(self, resource, editor, remove=False): - """ - Inform us that an editor has been opened for what is believed to be - a resource in this project. - - Note that if this project can be represented as a hierarchy, the - resource may not be a top level object in that hierarchy! - - """ - - # Warn if the resource is not part of this project - if not self._contains_resource(resource): - logger.warning( - "This Project [%s] does not contain resource [%s]", - self, - resource, - ) - - # Add or remove from our set of editors as requested - if not remove: - self._editors[resource] = editor - else: - del self._editors[resource] - - def save(self, location=None, overwrite=False): - """ - Save this project. - - The project is saved to its current location, identified by the value - of the *location* trait, unless a new location is explicitly provided. - The specification of a new location is used to do a 'save as' - operation. - - If a new location is provided, and a file or directory already exists - at that location, then an *AssertionError* exception is raised unless - the overwrite flag is True. This ensures that users won't accidentally - overwrite existing data. - - This method requires the overwrite flag because prompting the user to - confirm the overwrite requires GUI interaction and thus should not be - done at the model level. - - Note that, because we can rely on the location of a loaded project - always being set to the location it was loaded from, there is no reason - to try to force the *location* trait within a saved project to the - location we are trying to save to. Instead, we update the value in - the in-memory version of the project only if the save completed - successfully. - - The dirty flag is always cleared upon a succesfull save of the project. - - An exception will be raised to indicate a failure. - - """ - - # Ensure saving (or save as) is allowed at this time. - if location is None or location == self.location: - if not self.is_save_allowed: - raise AssertionError("Saving is currently not allowed.") - elif location != self.location: - if not self.is_save_as_allowed: - raise AssertionError("Save as is currently not allowed.") - - # Use the internally-specified location unless a new location was - # explicitly provided. The new location can not contain any starting - # or trailing whitespace and it cannot overwrite an existing file or - # directory unless that was explicitly allowed. - loc = self.location - if location is not None: - location = location.strip() - if len(location) > 0 and location != self.location: - - # Ensure we never overwrite existing files / directories just - # because someone specified a new location. (Confirmation or - # correction of overwriting requires prompting of the user and - # is thus not part of the project model.) - if os.path.exists(location) and overwrite is False: - raise AssertionError( - "Can not overwrite existing " - + "location [%s]" % location - ) - - # The requested location is valid so let's use it. - loc = location - - # Ensure all necessary directories exist. If we're saving a file, then - # these are the path upto the file name. If we're saving to a directory - # then the path is the complete location. - if self.PROJECTS_ARE_FILES: - path, filename = os.path.split(loc) - else: - path = loc - if len(path) > 0: - f = File(path) - if f.is_file: - f.delete() - if not f.exists: - f.create_folders() - - # Save this project in a manner that derived classes can modify. - self._save(loc) - - # If the save succeeds (no exceptions were raised), then update the - # location of the project and clear the dirty flag. - self.location = loc - self.dirty = False - - return - - def start(self): - """ - Notify this project that it is now the 'current' project. - - This call should only be made by the project plugin framework! - - This call *could* happen multiple times to a project, but - only if interwoven with paired calls to the 'stop' method. - - Derived classes should override this, and chain the base - implementation, if they need to do anything when a project - becomes current. - - """ - - logger.debug("Project [%s] started", self) - - # Ensure we start with an empty set of editors - self._editors = {} - - def stop(self): - """ - Called only by the project plugin framework to notify this - project it is no longer the current project. - - This call *could* happen multiple times to a project, but - only if interwoven with paired calls to the 'start' method. - - Derived classes should override this, and chain the base - implementation, if they need to do anything when a project - stops being current. - - """ - - # Close all of the editors displaying our resources - self._close_all_editors() - - logger.debug("Project [%s] stopped", self) - - #### protected interface ################################################# - - def _close_all_editors(self): - """ - Called to close all editors associated with this project. - - """ - - # NOTE: The close() method on the editor will call back to remove - # itself from our set of registered editors. (This assumes the - # editor is derived from ProjectEditor.) - for editor in self._editors.values(): - logger.debug( - "Project requesting close of ProjectEditor [%s]", editor - ) - editor.close() - - def _close_resource_editors(self, resource): - """ - Close any editors associated with the specified resource(s). - - The passed value may be a single resource or a list of resources. The - resources should be parts of this project but no error is generated if - they are not, nor if they are not currently associated with an editor. - - """ - - # Ensure we're dealing with a list of resources. - if not isinstance(resource, list): - resource = [resource] - - # Close any editors associated with the resources - for r in resource: - editor = self._editors.get(r, None) - if editor is not None: - logger.debug( - "Requesting close of ProjectEditor [%s] from " - + "Project [%s]", - editor, - self, - ) - editor.close() - - return - - def _contains_resource(self, resource): - """ - Called to determine if this project contains the specified - resource. - - Note that if this project can be represented as a hierarchy, the - resource may not be a top level object in that hierarchy! - - Derived classes must implement this! - - """ - - return False - - def _get_name(self): - """ - Returns the current name for this project. - - The name is always the last part of the path that is the project's - location. If we have no location, then a default name is returned. - - """ - - # Prefer to use the cached version of the name - if self._name is not None and len(self._name) > 0: - result = self._name - - # Use (and cache) a name from our current location - else: - # Strip any trailing path separator off the current location so - # that we use the last directory name if our location is a - # directory - location = self.location.rstrip(os.path.sep) - - # The project name is then the basename of the location - self._name = os.path.basename(location) - result = self._name - - return result - - def _load(cls, location): - """ - Load a project from the specified location. - - This method exists purely to allow derived classes to have an easy way - to override or extend the loading of a project. The default behavior - is to load the project from a file using the unpickle mechanism. - - The caller is notified of loading errors by raised exceptions. - - """ - - # Allow derived classes to determine the actual pickle file given the - # requested source location. - filename = cls.get_pickle_filename(location) - logger.debug("Loading Project of class [%s] from [%s]", cls, filename) - - # Try to unpickle the project while making sure to close any file we - # opened. - fh = None - try: - fh = open(filename, "rb") - pickle_package = cls.get_pickle_package() - project = pickle_package.load(fh) - - # Allow derived classes to customize behavior after unpickling - # is complete. - project._load_hook(location) - - logger.debug( - "Loaded Project [%s] from location [%s]", project, filename - ) - - # Ensure any opened file is closed - finally: - if fh: - try: - fh.close() - except: - logger.exception( - "Unable to close project file [%s]", filename - ) - - return project - - _load = classmethod(_load) - - def _load_hook(self, location): - """ - Finish loading of a project. - - This method exists purely to allow derived classes to customize the - steps that finish the loading of a project. - - Note that the project's internal location value does not yet reflect - the location the project was loaded from, nor do we guarantee the dirty - flag isn't set. (Doing the right thing to both of these is done by the - framework after this method!) - - """ - - pass - - def _location_default(self): - """ - Generates the default value for our location trait. - - """ - - return self.get_default_project_location(self.application) - - def _make_location_unique(cls, location): - """ - Return a location, based off the specified location, that does - not already exist on the filesystem. - - """ - - result = location - counter = 1 - while os.path.exists(result): - result = cls._unique_name_format % (location, counter) - counter += 1 - - return result - - _make_location_unique = classmethod(_make_location_unique) - - def _save(self, location): - """ - Save this project to the specified location. - - This method exists purely to allow derived classes to have an easy way - to override or extend the saving of a project. The default behavior is - to save this project to a file using the pickle mechanism. - - The caller is notified of saving errors by raised exceptions. - - """ - - # Allow derived classes to determine the actual pickle file from the - # specified location. - filename = self.get_pickle_filename(location) - logger.debug("Saving Project [%s] to [%s]", self, filename) - - # Pickle the object to a file while making sure to close any file we - # open. Note that we can't just log or ignore errors here as the - # caller needs to know whether we succeeded or not, and could possibly - # handle the exception if they knew what it was. - fh = None - try: - # Allow derived classes to customize behavior before pickling is - # applied. - self._save_hook(location) - - fh = open(filename, "wb") - pickle_package = self.get_pickle_package() - pickle_package.dump(self, fh, 1) - - logger.debug("Saved Project [%s] to [%s]", self, filename) - finally: - try: - if fh is not None: - fh.close() - except: - logger.exception( - "Unable to close project pickle file [%s]", filename - ) - - return - - def _save_hook(self, location): - """ - Start saving a project. - - This method exists purely to allow derived classes to customize the - steps that initiate the saving of a project. - - Note that the project's internal location value does not reflect the - target location for the save. - - """ - - pass - - #### trait handlers ###################################################### - - def _location_changed(self, old, new): - """ - Called whenever the project's location changes. - - """ - - logger.debug( - "Location changed from [%s] to [%s] for Project [%s]", - old, - new, - self, - ) - - # Invalidate any cached project name - old_name = self._name - self._name = "" - self.trait_property_changed("name", old_name, self.name) - - # Indicate this project is now dirty - self.dirty = True - - -# Helper functions - - -def clean_filename(name, replace_empty=""): - """ - Make a user-supplied string safe for filename use. - - Returns an ASCII-encodable string based on the input string that's safe for - use as a component of a filename or URL. The returned value is a string - containing only lowercase ASCII letters, digits, and the characters '-' and - '_'. - This does not give a faithful representation of the original string: - different input strings can result in the same output string. - - Parameters - ---------- - name : str - The string to be made safe. - replace_empty : str, optional - The return value to be used in the event that the sanitised - string ends up being empty. No validation is done on this - input - it's up to the user to ensure that the default is - itself safe. The default is to return the empty string. - - Returns - ------- - safe_string : str - A filename-safe version of string. - """ - # Code is based on Django's slugify utility. - # https://docs.djangoproject.com/en/1.9/_modules/django/utils/text/#slugify - name = ( - unicodedata.normalize("NFKD", name) - .encode("ascii", "ignore") - .decode("ascii") - ) - name = re.sub(r"[^\w\s-]", "", name).strip().lower() - safe_name = re.sub(r"[-\s]+", "-", name) - if safe_name == "": - return replace_empty - return safe_name diff --git a/envisage/ui/single_project/project_action.py b/envisage/ui/single_project/project_action.py deleted file mode 100644 index c717b2a08..000000000 --- a/envisage/ui/single_project/project_action.py +++ /dev/null @@ -1,245 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" -A base class for actions that determine their enabled status based on -the state of the current project. - -""" - -# Standard library imports -import logging -import inspect - -# Enthought library imports -from envisage.api import Application -from envisage.ui.workbench.api import WorkbenchWindow -from pyface.action.api import Action -from traits.api import Instance, Str - -# Local imports. -from envisage.ui.single_project.services import IPROJECT_MODEL - - -# Setup a logger for this module. -logger = logging.getLogger(__name__) - - -class ProjectAction(Action): - """ - A base class for actions that determine their enabled status based on - the state of the current project. - - This class is architected such that implementors can override the - 'refresh' method without losing the default logic that determines - the enabled state as a function of the current project. - - """ - - #### 'ProjectAction' interface #################################### - - # The application that the action is part of. This is a convenience - # property and is equivalent to 'self.window.application'. - application = Instance(Application) - - # The project model service we refresh our state against. - model_service = Instance(IPROJECT_MODEL) - - # The universal object locator (UOL). This string is used to locate an - # instance to invoke a method on. - # - # UOLs can currently have one of the following forms: - # - # * ``'service://a_service_identifier'`` - # * ``'name://a/path/through/the/naming/service'`` - # * ``'file://the/pathname/of/a/file/containing/a/UOL'`` - # * ``'http://a/URL/pointing/to/a/text/document/containing/a/UOL'`` - uol = Str - - # The name of the method to invoke on the object. - method_name = Str - - ########################################################################## - # 'object' interface - ########################################################################## - - def __init__(self, *args, **kws): - """ - Constructor. - - Extended to setup a listener on the project model service so - that we can refresh whenever the project changes. - - """ - - # Retrieve the project model service and register ourself to listen - # for project state changes. This is done first to avoid errors - # during any refresh calls triggered by further initialization. - # FIXME: I don't think my implementation of the ProjectAction class is correct - # because I can't see to get a reference to the current application. Because of - # this, I'm not able to setup the listeners yet but this needs to be done eventually! - """ - if 'model_service' in kws: - self.model_service = kws['model_service'] - del kws['model_service'] - else: - self.model_service = self.window.application.get_service(IPROJECT_MODEL) - self._update_model_service_listeners(remove=False) - """ - - super().__init__(*args, **kws) - - return - - ########################################################################## - # 'Action' interface - ########################################################################## - - #### public interface #################################################### - - def destroy(self): - """ - Destroy this action. - - Overridden here to remove our project model service listeners. - - """ - - self._update_model_service_listeners(remove=True) - - return - - ########################################################################## - # 'ProjectAction' interface - ########################################################################## - - #### public interface #################################################### - - def refresh(self): - """ - Refresh the enabled state of this action. - - This default implementation enables the action only when there is a - current project. - - """ - - self.enabled = self._refresh_project_exists() - - return - - def perform(self, event): - """ Performs the action. - - This implementation simply performs the action specified by **uol** - and **method_name**. - - Override this method to add additional work to the performance of this - action. - - """ - - self._perform_uol_action(event) - - return - - ########################################################################### - # Private interface. - ########################################################################### - - def _perform_uol_action(self, event): - """ Called to perform the configured UOL action. """ - - # Find the object. - object = event.window.application.get_service(self.uol) - if object is not None: - method = getattr(object, self.method_name) - - # If the only argument is 'self' then don't pass the event. This - # makes it easier to hook up actions that invoke NON-UI methods. - # - # fixme: Should we check the argument spec of the method more - # closely and only pass the event iff there is exactly one argument - # called 'event'? - args, varargs, varkw, dflts = inspect.getargspec(method) - if len(args) == 1: - method() - - else: - method(event) - - else: - logger.error("Cannot resolve UOL: %s" % self.uol) - - return - - #### protected interface ################################################# - - def _refresh_project_exists(self): - """ - Return the refresh state according to whether the model service has a - current project. - - Returns True if this action should be enabled. False otherwise. - - """ - - enabled = False - if ( - self.model_service is not None - and self.model_service.project is not None - ): - enabled = True - - return enabled - - def _update_model_service_listeners(self, remove=False): - """ - Update our listeners on the project model service. - - These are done as individual listener methods so that derived - classes can change the behavior when a single event occurs. - - """ - - logger.debug( - (remove and "Removing " or "Adding ") - + "listeners on project model service for ProjectAction [%s]", - self, - ) - - self.model_service.on_trait_change( - self._on_project_changed, "project", remove=remove - ) - self.model_service.on_trait_change( - self._on_project_selection_changed, "selection", remove=remove - ) - - return - - #### trait handlers ###################################################### - - def _on_project_changed(self, obj, trait_name, old, new): - """ - Handle changes to the value of the current project. - - """ - - self.refresh() - - return - - def _on_project_selection_changed(self, obj, trait_name, old, new): - """ - Handle changes to the selection value within the current project. - - """ - - self.refresh() - - return diff --git a/envisage/ui/single_project/project_action_set.py b/envisage/ui/single_project/project_action_set.py deleted file mode 100644 index 4da2d587c..000000000 --- a/envisage/ui/single_project/project_action_set.py +++ /dev/null @@ -1,114 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" Single project action set. """ -# Enthought library imports. -from envisage.ui.action.api import Action, Group, Menu, ToolBar -from envisage.ui.workbench.api import WorkbenchActionSet - -# This module's package. -PKG = ".".join(__name__.split(".")[:-1]) - - -class ProjectActionSet(WorkbenchActionSet): - """ Action set of a default Project. """ - - # The action set's globally unique identifier. - id = "envisage.ui.single_project.action_set" - - # List of menus we provide. - menus = [ - Menu( - id="ProjectMenu", - name="&Project", - path="MenuBar/File", - group="ProjectGroup", - ), - ] - - # List of groups we provide. - groups = [ - Group(id="OpenGroup", path="MenuBar/File/ProjectMenu"), - Group(id="SaveGroup", path="MenuBar/File/ProjectMenu"), - Group(id="CloseGroup", path="MenuBar/File/ProjectMenu"), - Group(id="ProjectGroup", path="MenuBar/File", before="ExitGroup"), - ] - - # List of toolbars we provide. - tool_bars = [ - ToolBar(name="Project", groups=["PerspectiveGroup", "ProjectGroup"]), - ] - - # List of actions we provide. - actions = [ - # File menu actions. - Action( - class_name=PKG + ".action.api:NewProjectAction", - group="OpenGroup", - path="MenuBar/File/ProjectMenu", - ), - Action( - class_name=PKG + ".action.api:OpenProjectAction", - group="OpenGroup", - path="MenuBar/File/ProjectMenu", - ), - Action( - class_name=PKG + ".action.api:SaveProjectAction", - group="SaveGroup", - path="MenuBar/File/ProjectMenu", - ), - Action( - class_name=PKG + ".action.api:SaveAsProjectAction", - group="SaveGroup", - path="MenuBar/File/ProjectMenu", - ), - Action( - class_name=PKG + ".action.api:CloseProjectAction", - group="CloseGroup", - path="MenuBar/File/ProjectMenu", - ), - # Toolbar actions. - Action( - class_name=PKG + ".action.api:SwitchToAction", - group="PerspectiveGroup", - path="ToolBar/Project", - ), - Action( - class_name=PKG + ".action.api:NewProjectAction", - group="ProjectGroup", - path="ToolBar/Project", - ), - Action( - class_name=PKG + ".action.api:OpenProjectAction", - group="ProjectGroup", - path="ToolBar/Project", - ), - Action( - class_name=PKG + ".action.api:SaveProjectAction", - group="ProjectGroup", - path="ToolBar/Project", - ), - Action( - class_name=PKG + ".action.api:SaveAsProjectAction", - group="ProjectGroup", - path="ToolBar/Project", - ), - Action( - class_name=PKG + ".action.api:CloseProjectAction", - group="ProjectGroup", - path="ToolBar/Project", - ), - ] - - #### 'WorkbenchActionSet' interface ####################################### - - # The Ids of the perspectives that the action set is enabled in. - enabled_for_perspectives = ["Project"] - - # The Ids of the perspectives that the action set is visible in. - visible_for_perspectives = ["Project"] diff --git a/envisage/ui/single_project/project_factory.py b/envisage/ui/single_project/project_factory.py deleted file mode 100644 index 221986ffc..000000000 --- a/envisage/ui/single_project/project_factory.py +++ /dev/null @@ -1,89 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" -A base class for project factories. - -""" - -# Standard library imports -import logging - -# Enthought library imports -from envisage.api import IApplication -from traits.api import HasTraits, Instance - -# Local imports. -from .project import Project - - -# Setup a logger for this module. -logger = logging.getLogger(__name__) - - -class ProjectFactory(HasTraits): - """ - A base class for project factories. - - """ - - ########################################################################## - # Attributes - ########################################################################## - - #### public 'ProjectFactory' interface ################################### - - # The class of the project created by this factory. - # - # This is provided so that the single_project services can call class - # methods. - # - # This value is meant to be constant for the lifetime of this class! - PROJECT_CLASS = Project - - # Current envisage application. - application = Instance(IApplication) - - ########################################################################## - # 'ProjectFactory' interface. - ########################################################################## - - #### public method ####################################################### - - def create(self): - """ - Create a new project from scratch. - - This must return an instance of a Project or 'None'. A return - value of 'None' indicates that no project could be created. The - plugin will display the default traits view to the user so that - they can configure this new project. - - """ - - return self.PROJECT_CLASS(application=self.application) - - def open(self, location): - """ - Open a project from the specified location. - - This must return an instance of a Project or 'None'. A return - value of 'None' indicates that no project could be opened from - the specified location. - - """ - - try: - project = self.PROJECT_CLASS.load(location, self.application) - except: - logger.exception( - "Unable to load Project from location %s", location - ) - project = None - - return project diff --git a/envisage/ui/single_project/project_plugin.py b/envisage/ui/single_project/project_plugin.py deleted file mode 100644 index e6106afe9..000000000 --- a/envisage/ui/single_project/project_plugin.py +++ /dev/null @@ -1,472 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" The Envisage single project plugin. """ - -# Standard library imports -import logging - -# Enthought library imports. -from envisage.api import ExtensionPoint, Plugin, ServiceOffer -from envisage.ui.single_project.api import FactoryDefinition -from pyface.action.api import MenuManager -from pyface.workbench.api import Perspective -from traits.api import Callable, List - -# Local imports. -from .model_service import ModelService -from .project_action_set import ProjectActionSet -from .services import IPROJECT_MODEL, IPROJECT_UI -from .ui_service_factory import UIServiceFactory - -# This module's package. -PKG = ".".join(__name__.split(".")[:-1]) - -# Setup a logger for this module. -logger = logging.getLogger(__name__) - - -############################################################################### -# `ProjectPerspective` class. -############################################################################### - -class ProjectPerspective(Perspective): - """ - A default perspective for the single_project plugin. - - """ - - # The perspective's name. - name = "Project" - - # Should this perspective be enabled or not? - enabled = True - - # Should the editor area be shown in this perspective? - show_editor_area = True - - # The contents of the perspective. - # TODO: Setup the PerspectiveItems based on the areas in our perspective. - # contents = [] - - -############################################################################## -# 'ProjectPlugin' class. -############################################################################## -class ProjectPlugin(Plugin): - """ - The single-project plugin. - - """ - - # The Ids of the extension points that this plugin offers. - ACTION_SETS = "envisage.ui.workbench.action_sets" - FACTORY_DEFINITIONS = "envisage.ui.single_project.factory_definitions" - UI_SERVICE_FACTORY = "envisage.ui.single_project.ui_service_factory" - - # The Ids of the extension points that this plugin contributes to. - PERSPECTIVES = "envisage.ui.workbench.perspectives" - PREFERENCES = "envisage.preferences" - PREFERENCES_PAGES = "envisage.ui.workbench.preferences_pages" - SERVICE_OFFERS = "envisage.service_offers" - VIEWS = "envisage.ui.workbench.views" - - #### 'IPlugin' interface ################################################## - - # The plugin's unique identifier. - id = "envisage.ui.single_project" - - # The plugin's name (suitable for displaying to the user). - name = "Single Project" - - #### Extension points offered by this plugin ############################## - - # Factory definitions. - factory_definitions = ExtensionPoint( - List(Callable), - id=FACTORY_DEFINITIONS, - desc=""" - - A project factory definition. - - An instance of the specified class is used to open and/or create new - projects. - - The extension with the highest priority wins! In the event of a tie, - the first instance wins. - - """, - ) - - # Ui service factories. - ui_service_factory = ExtensionPoint( - List(Callable), - id=UI_SERVICE_FACTORY, - desc=""" - - A ui service factory definition. - - """, - ) - - #### Contributions to extension points made by this plugin ################ - - # Action sets. - action_sets = List(contributes_to=ACTION_SETS) - - def _action_sets_default(self): - """ - Default project actions. - - """ - - return [ProjectActionSet] - - # Factory definitions. - my_factory_definitions = List(contributes_to=FACTORY_DEFINITIONS) - - def _my_factory_definitions_default(self): - """ - Default factory definition. - - """ - - factory_definition = FactoryDefinition( - class_name=PKG + ".project_factory.ProjectFactory", priority=0, - ) - - return [factory_definition] - - # Perspectives. - perspectives = List(contributes_to=PERSPECTIVES) - - def _perspectives_default(self): - """ - Default project perspective. - - """ - - return [ProjectPerspective] - - # Service offers. - service_offers = List(contributes_to=SERVICE_OFFERS) - - def _service_offers_default(self): - """ - Our service contributions. - - """ - # FIXME: Eventually we will register the services here intead - # of in the plugin's start() method. - # return [model_service, ui_service] - - return [] - - # Ui service factories. - my_ui_service_factory = List(contributes_to=UI_SERVICE_FACTORY) - - def _my_ui_service_factory_default(self): - """ - Default ui service factory. - - """ - - ui_service_factory = UIServiceFactory( - class_name=PKG + ".ui_service_factory.UIServiceFactory", - priority=0, - ) - - return [ui_service_factory] - - # Preferences. - my_preferences = List(contributes_to=PREFERENCES) - - def _my_preferences_default(self): - """ - Default preferences. - - """ - return ["pkgfile://%s/preferences.ini" % PKG] - - # Preference pages. - my_preferences_pages = List(contributes_to=PREFERENCES_PAGES) - - def _my_preferences_pages_default(self): - """ - Default preference page. - - """ - - from .default_path_preference_page import DefaultPathPreferencePage - - return [DefaultPathPreferencePage] - - # Views. - views = List(contributes_to=VIEWS) - - def _views_default(self): - """ - Add our project views. - - """ - - return [self._project_view_factory] - - ### protected interface ################################################## - - def start(self): - """ - Starts the plugin. - - Overridden here to start up our services and load the project - that was open when we were last shut-down. - - """ - - super().start() - - # FIXME: We eventually won't have to explicitly register the - # services ourselves, since we contribute them as service offers - # so they are instantiated when they are invoked, but since they are - # not used anywhere else yet, I had to use this same old approach - # just to test and make sure they were working correctly. - # Create and register the model service we offer - model_service = self._create_model_service() - self.application.register_service(IPROJECT_MODEL, model_service) - - # Create and register the ui service we offer - ui_service = self._create_ui_service(model_service) - self.application.register_service(IPROJECT_UI, ui_service) - - # Set up any listeners interested in the current project selection - # FIXME: Register the selection listeners for the current project selection. - # self._register_selection_listeners(model_service) - - return - - ###################################################################### - # Private methods. - def _project_view_factory(self, window, **traits): - """ - Factory method for project views. - - """ - from pyface.workbench.traits_ui_view import TraitsUIView - from envisage.ui.single_project.api import ProjectView - - project_view = ProjectView(application=window.application) - tui_project_view = TraitsUIView( - obj=project_view, - id="envisage.ui.single_project.view.project_view.ProjectView", - name="Project View", - window=window, - position="left", - **traits, - ) - return tui_project_view - - def _create_model_service(self): - """ - Creates a model service for this plugin. - - """ - - # Determine which contributed project factory to use. - factory = self._get_contributed_project_factory() - - # Make sure the factory has a reference to our Envisage application. - factory.application = self.application - - # Create the project service instance. - result = ModelService(self.application, factory) - - return result - - def _create_ui_service(self, model_service): - """ - Creates a UI service for this plugin. - - """ - - # Create the menu manager representing the context menu we show when - # nothing is selected in the project view. - menu_manager = self._get_no_selection_context_menu_manager() - - # Get the UI service factory. - ui_service_factory = self._get_contributed_ui_service_factory() - - # Create the ui service instance - ui_service = ui_service_factory.create_ui_service( - model_service, menu_manager - ) - - return ui_service - - def _get_contributed_project_factory(self): - """ - Retrieves the instance of the project factory to use with this - plugin. - - The instance is generated from the contributed factory definition - that was the first one with the highest priority. - - """ - - # Retrieve all the factory definition contributions - extensions = self.application.get_extensions( - "envisage.ui.single_project.factory_definitions" - ) - - # Find the winning contribution - definition = None - for extension in extensions: - if not definition or extension.priority > definition.priority: - definition = extension - - # Create an instance of the winning project factory - logger.info("Using ProjectFactory [%s]", definition.class_name) - klass = self.application.import_symbol(definition.class_name) - factory = klass() - - return factory - - def _get_contributed_ui_service_factory(self): - """ - Retrieves the instance of the UiService factory to use with this - plugin. - - The instance is generated from the contributed factory definition - that was the first one with the highest priority. - - """ - - # Retrieve all the factory definition contributions - extensions = self.get_extensions( - "envisage.ui.single_project.ui_service_factory" - ) - - # Find the winning contribution - definition = None - for extension in extensions: - if not definition or extension.priority > definition.priority: - definition = extension - - # Create an instance of the winning factory - logger.info("Using UiService Factory [%s]", definition.class_name) - class_name = definition.class_name - klass = self.application.import_symbol(class_name) - factory = klass() - - return factory - - def _get_no_selection_context_menu_manager(self): - """ - Generates a menu manager representing the context menu shown when - nothing is selected within the project view. That is, when the - user right clicks on any empty space within our associated UI. - - """ - - # Retrieve all contributions for the no-selection context menu. - extensions = self.get_extensions(ProjectActionSet) - - # Populate a menu manager from the extensions. - menu_manager = MenuManager() - if len(extensions) > 0: - action_set_manager = ActionSetManager(action_sets=extensions) - menu_builder = DefaultMenuBuilder(application=self.application) - menu_builder.initialize_menu_manager( - menu_manager, action_set_manager, NO_SELECTION_MENU_ID - ) - - return menu_manager - - def _register_selection_listeners(self, model_service): - """ - Registers any extension-requested listeners on the project - selection. - - """ - - for sps in self.get_extensions(SyncProjectSelection): - object = self.application.lookup_application_object(sps.uol) - if object is not None: - name = sps.name - self._register_selection_handler(object, name, model_service) - else: - logger.error( - "Could not resolve the SyncProjectSelection " - + 'UOL: "%s"', - sps.uol, - ) - - return - - def _register_selection_handler(self, object, name, model_service): - """ - Creates a handler and registers it. - - """ - - def handler(): - # The key to this method is to realize that our goal is to - # make it as easy as possible to create recipients for - # notification. Using traits as the recipients makes - # creation very simple because we can rely on the type - # knowledge within that trait to ensure only valid values - # get assigned to the recipient. That is the recipient - # doesn't need to do anything complex to validate the - # values they get assigned. This method also works if the - # recipient isn't a trait, but in that case, they will - # have to handle multiple selection of the project - # bindings. - # - # First, try to provide the recipient with a multiple - # selection type value i.e. a list of bindings. - try: - setattr(object, name, model_service.selection) - return - except: - pass - - # If that didn't work, remove the binding wrappers and try - # notification of the resulting list. - selection = [s.obj for s in model_service.selection] - try: - setattr(object, name, selection) - return - except: - pass - - # If that didn't work, and only a single item is selected, - # then try to provide that item to the recipient. - if len(selection) == 1: - try: - setattr(object, name, selection[0]) - return - except: - pass - - # The recipient must not be accepting the type of the - # current selection, so let's clear its current selection - # instead. If this fails, then something has gone wrong - # with the declaration of the recipient. - try: - setattr(object, name, None) - except: - logger.debug( - "Error informing object [%s] of project " - "selection change via attribute [%s]", - object, - name, - ) - - model_service.on_trait_change(handler, "selection") - model_service.on_trait_change(handler, "selection_items") - - return diff --git a/envisage/ui/single_project/services.py b/envisage/ui/single_project/services.py deleted file mode 100644 index 0691bb130..000000000 --- a/envisage/ui/single_project/services.py +++ /dev/null @@ -1,11 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -# IDs of services provided by this plugin -IPROJECT_MODEL = "envisage.ui.single_project.model_service.ModelService" -IPROJECT_UI = "envisage.ui.single_project.ui_service.UiService" diff --git a/envisage/ui/single_project/tests/__init__.py b/envisage/ui/single_project/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/envisage/ui/single_project/tests/test_clean_strings.py b/envisage/ui/single_project/tests/test_clean_strings.py deleted file mode 100644 index e0bbac2e5..000000000 --- a/envisage/ui/single_project/tests/test_clean_strings.py +++ /dev/null @@ -1,73 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! - -import unittest - -from envisage.ui.single_project.project import clean_filename - -# Safe strings should only contain the following characters. -LEGAL_CHARS = set("-0123456789_abcdefghijklmnopqrstuvwxyz") - - -class TestCleanStrings(unittest.TestCase): - def test_clean_filename_default(self): - test_strings = [ - "!!!", - "", - " ", - "\t/\n", - "^!+", - ] - for test_string in test_strings: - safe_string = clean_filename(test_string, "default-output") - self.check_output(safe_string) - self.assertEqual(safe_string, "default-output") - - def test_clean_filename_whitespace_handling(self): - # Leading and trailing whitespace stripped. - self.assertEqual(clean_filename(" abc "), "abc") - self.assertEqual(clean_filename(" \t\tabc \n"), "abc") - # Internal whitespace turned into hyphens. - self.assertEqual(clean_filename("well name"), "well-name") - self.assertEqual(clean_filename("well \n name"), "well-name") - self.assertEqual(clean_filename("well - name"), "well-name") - - def test_clean_filename_conversion_to_lowercase(self): - test_string = "ABCdefGHI123" - safe_string = clean_filename(test_string) - self.assertEqual(safe_string, test_string.lower()) - self.check_output(safe_string) - - def test_clean_filename_accented_chars(self): - test_strings = [ - "\xe4b\xe7d\xe8f", - "a\u0308bc\u0327de\u0300f", - ] - for test_string in test_strings: - safe_string = clean_filename(test_string) - self.check_output(safe_string) - self.assertEqual(safe_string, "abcdef") - - def test_clean_filename_all_chars(self): - test_strings = [ - "".join(chr(n) for n in range(10000)), - "".join(chr(n) for n in range(10000)) * 2, - "".join(chr(n) for n in reversed(range(10000))), - ] - for test_string in test_strings: - safe_string = clean_filename(test_string) - self.check_output(safe_string) - - def check_output(self, safe_string): - """ - Check that a supposedly safe string is actually safe. - """ - self.assertIsInstance(safe_string, str) - chars_in_string = set(safe_string) - self.assertLessEqual(chars_in_string, LEGAL_CHARS) diff --git a/envisage/ui/single_project/tests/test_project_view.py b/envisage/ui/single_project/tests/test_project_view.py deleted file mode 100644 index da3ba294e..000000000 --- a/envisage/ui/single_project/tests/test_project_view.py +++ /dev/null @@ -1,45 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! - -import unittest - -from traits.api import HasTraits, Supports -from traits.interface_checker import check_implements -from traitsui.api import ITreeNode - -from envisage.ui.single_project.api import Project -from envisage.ui.single_project.view.project_view import EmptyProject - - -class MyProject(Project): - pass - - -class MyView(HasTraits): - tree_node = Supports(ITreeNode) - - -class TestProjectView(unittest.TestCase): - def test_empty_project_adapts_to_i_tree_node(self): - empty_project = EmptyProject() - - view = MyView() - view.tree_node = empty_project - - adapted = view.tree_node - check_implements(type(adapted), ITreeNode) - - def test_project_adapts_to_i_tree_node(self): - my_project = MyProject() - - view = MyView() - view.tree_node = my_project - - adapted = view.tree_node - check_implements(type(adapted), ITreeNode) diff --git a/envisage/ui/single_project/ui_service.py b/envisage/ui/single_project/ui_service.py deleted file mode 100644 index 38680c828..000000000 --- a/envisage/ui/single_project/ui_service.py +++ /dev/null @@ -1,816 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" -A service to enable UI interactions with the single project plugin. - -""" - -# Standard library imports. -import logging -import os -import shutil - -# Enthought library imports -from apptools.preferences.api import bind_preference -from apptools.io.api import File -from apptools.naming.api import Context -from pyface.api import ( - CANCEL, - confirm, - ConfirmationDialog, - DirectoryDialog, - error, - FileDialog, - information, - NO, - OK, - YES, -) -from pyface.action.api import MenuManager -from pyface.timer.api import do_later, Timer -from traits.api import Any, Event, HasTraits, Instance, Int - -# Local imports. -from .model_service import ModelService - - -# Setup a logger for this module. -logger = logging.getLogger(__name__) - - -class UiService(HasTraits): - """ - A service to enable UI interactions with the single project plugin. - - """ - - ########################################################################## - # Attributes - ########################################################################## - - #### public 'UiService' interface ######################################## - - # The manager of the default context menu - default_context_menu_manager = Instance(MenuManager) - - # A reference to our plugin's model service. - model_service = Instance(ModelService) - - # The project control (in our case a tree). This is created by the - # project view. Provided here so that sub-classes may access it. - project_control = Any - - # Fired when a new project has been created. The value should be the - # project instance that was created. - project_created = Event - - # A timer to implement automatic project saving. - timer = Instance(Timer) - - # The interval (minutes)at which automatic saving should occur. - autosave_interval = Int(5) - - ########################################################################## - # 'object' interface. - ########################################################################## - - #### operator methods #################################################### - - def __init__(self, model_service, menu_manager, **traits): - """ - Constructor. - - Extended to require a reference to the plugin's model service to create - an instance. - - """ - - super().__init__( - model_service=model_service, - default_context_menu_manager=menu_manager, - **traits, - ) - try: - # Bind the autosave interval to the value specified in the - # single project preferences - p = self.model_service.preferences - bind_preference(self, "autosave_interval", 5, p) - except: - logger.exception( - "Failed to bind autosave_interval in [%s] to " - "preferences." % self - ) - - return - - ########################################################################## - # 'UiService' interface. - ########################################################################## - - #### public interface #################################################### - - def close(self, event): - """ - Close the current project. - - """ - - # Ensure any current project is ready for this change. - if self.is_current_project_saved(event.window.control): - - # If we have a current project, close it. - current = self.model_service.project - if current is not None: - logger.debug("Closing Project [%s]", current.name) - self.model_service.project = None - - return - - def create(self, event): - """ - Create a new project. - - """ - # Ensure any current project is ready for this change. - if self.is_current_project_saved(event.window.control): - - # Use the registered factory to create a new project - project = self.model_service.factory.create() - if project is not None: - - # Allow the user to customize the new project - dialog = project.edit_traits( - parent=event.window.control, - # FIXME: Due to a bug in traits, using a wizard dialog - # causes all of the Instance traits on the object being - # edited to be replaced with new instances without any - # listeners on those traits being called. Since we can't - # guarantee that our project's don't have Instance traits, - # we can't use the wizard dialog type. - # kind = 'wizard' - kind="livemodal", - ) - - # If the user closed the dialog with an ok, make it the - # current project. - if dialog.result: - logger.debug("Created Project [%s]", project.name) - self.model_service.project = project - self.project_created = project - - return - - def display_default_context_menu(self, parent, event): - """ - Display the default context menu for the plugin's ui. This is the - context menu used when neither a project nor the project's contents - are right-clicked. - - """ - - # Determine the current workbench window. This should be safe since - # we're only building a context menu when the user clicked on a - # control that is contained in a window. - workbench = self.model_service.application.get_service( - "envisage.ui.workbench.workbench.Workbench" - ) - window = workbench.active_window - - # Build our menu - from envisage.workbench.action.action_controller import ( - ActionController, - ) - - menu = self.default_context_menu_manager.create_menu( - parent, controller=ActionController(window=window) - ) - - # Popup the menu (if an action is selected it will be performed - # before before 'PopupMenu' returns). - if menu.GetMenuItemCount() > 0: - menu.show(event.x, event.y) - - return - - def delete_selection(self): - """ - Delete the current selection within the current project. - - """ - - # Only do something if we have a current project and a non-empty - # selection - current = self.model_service.project - selection = self.model_service.selection[:] - if current is not None and len(selection) > 0: - logger.debug("Deleting selection from Project [%s]", current) - - # Determine the context for the current project. Raise an error - # if we can't treat it as a context as then we don't know how - # to delete anything. - context = self._get_context_for_object(current) - if context is None: - raise Exception( - "Could not treat Project " + "[%s] as a context" % current - ) - - # Filter out any objects in the selection that can NOT be deleted. - deletables = [] - for item in selection: - rt = self._get_resource_type_for_object(item.obj) - nt = rt.node_type - if nt.can_delete(item): - deletables.append(item) - else: - logger.debug( - "Node type reports selection item [%s] is " - "not deletable.", - nt, - ) - - if deletables != []: - # Confirm the delete operation with the user - names = "\n\t".join([b.name for b in deletables]) - message = ( - "You are about to delete the following selected " - "items:\n\t%s\n\n" - "Are you sure?" - ) % names - title = "Delete Selected Items?" - action = confirm(None, message, title) - if action == YES: - - # Unbind all the deletable nodes - if len(deletables) > 0: - self._unbind_nodes(context, deletables) - - return - - def is_current_project_saved(self, parent_window): - """ - Give the user the option to save any modifications to the current - project prior to closing it. - - If the user wanted to cancel the closing of the current project, - this method returns False. Otherwise, it returns True. - - """ - - # The default is the user okay'd the closing of the project - result = True - - # If the current project is dirty, handle that now by challenging the - # user for how they want to handle them. - current = self.model_service.project - if not (self._get_project_state(current)): - dialog = ConfirmationDialog( - parent=parent_window, - cancel=True, - title="Unsaved Changes", - message='Do you want to save the changes to project "%s"?' - % (current.name), - ) - action = dialog.open() - if action == CANCEL: - result = False - elif action == YES: - result = self._save(current, parent_window) - elif action == NO: - # Delete the autosaved file as the user does not wish to - # retain the unsaved changes. - self._clean_autosave_location(current.location.strip()) - return result - - def listen_for_application_exit(self): - """ - Ensure that we get notified of any attempts to, and thus have a chance - to veto, the closing of the application. - - FIXME: Normally this should be called during startup of this - plugin, however, Envisage won't let us find the workbench service - then because we've made a contribution to its extension points - and it insists on starting us first. - - """ - - workbench = self.model_service.application.get_service( - "envisage.ui.workbench.workbench.Workbench" - ) - workbench.on_trait_change(self._workbench_exiting, "exiting") - - return - - def open(self, event): - """ - Open a project. - - """ - # Ensure any current project is ready for this change. - if self.is_current_project_saved(event.window.control): - - # Query the user for the location of the project to be opened. - path = self._show_open_dialog(event.window.control) - if path is not None: - logger.debug("Opening project from location [%s]", path) - - project = self.model_service.factory.open(path) - if project is not None: - logger.debug("Opened Project [%s]", project.name) - self.model_service.project = project - else: - msg = "Unable to open %s as a project." % path - error( - event.window.control, msg, title="Project Open Error" - ) - - return - - def save(self, event): - """ - Save a project. - - """ - - current = self.model_service.project - if current is not None: - self._save(current, event.window.control) - - return - - def save_as(self, event): - """ - Save the current project to a different location. - - """ - - current = self.model_service.project - if current is not None: - self._save(current, event.window.control, prompt_for_location=True) - - return - - #### protected interface ################################################# - - def _auto_save(self, project): - """ - - Called periodically by the timer's Notify function to automatically - save the current project. - The auto-saved project has the extension '.autosave'. - - """ - # Save the project only if it has been modified. - if project.dirty and project.is_save_as_allowed: - location = project.location.strip() - if not (location is None or len(location) < 1): - autosave_loc = self._get_autosave_location(location) - try: - # We do not want the project's location and name to be - # updated. - project.save(autosave_loc, overwrite=True, autosave=True) - msg = "[%s] auto-saved to [%s]" % (project, autosave_loc) - logger.debug(msg) - except: - logger.exception( - "Error auto-saving project [%s]" % project - ) - else: - logger.exception( - "Error auto-saving project [%s] in " - "location %s" % (project, location) - ) - return - - def _clean_autosave_location(self, location): - """ - Removes any existing autosaved files or directories for the project - at the specified location. - - """ - autosave_loc = self._get_autosave_location(location) - if os.path.exists(autosave_loc): - self.model_service.clean_location(autosave_loc) - return - - def _get_autosave_location(self, location): - """ - Returns the path for auto-saving the project in location. - - """ - return os.path.join( - os.path.dirname(location), os.path.basename(location) + ".autosave" - ) - - def _get_context_for_object(self, obj): - """ - Return the context for the specified object. - - """ - - if isinstance(obj, Context): - context = obj - else: - context = None - resource_type = self._get_resource_type_for_object(obj) - if resource_type is not None: - factory = resource_type.context_adapter_factory - if factory is not None: - # FIXME: We probably should use a real environment and - # context (parent context?) - context = factory.adapt(obj, Context, {}, None) - - return context - - def _get_resource_type_for_object(self, obj): - """ - Return the resource type for the specified object. - - If no type could be found, returns None. - - """ - - resource_manager = self.model_service.resource_manager - return resource_manager.get_type_of(obj) - - def _get_project_state(self, project): - """ Returns True if the project is clean: i.e., the dirty flag is - False and all autosaved versions have been deleted from the filesystem. - - """ - - result = True - if project is not None: - autosave_loc = self._get_autosave_location( - project.location.strip() - ) - if project.dirty or os.path.exists(autosave_loc): - result = False - return result - - def _get_user_location(self, project, parent_window): - """ - Prompt the user for a new location for the specified project. - - Returns the chosen location or, if the user cancelled, an empty - string. - - """ - - # The dialog to use depends on whether we're prompting for a file or - # a directory. - if self.model_service.are_projects_files(): - dialog = FileDialog( - parent=parent_window, - title="Save Project As", - default_path=project.location, - action="save as", - ) - title_type = "File" - else: - dialog = DirectoryDialog( - parent=parent_window, - message="Choose a Directory for the Project", - default_path=project.location, - action="open", - ) - title_type = "Directory" - - # Prompt the user for a new location and then validate we're not - # overwriting something without getting confirmation from the user. - result = "" - while dialog.open() == OK: - location = dialog.path.strip() - - # If the chosen location doesn't exist yet, we're set. - if not os.path.exists(location): - logger.debug("Location [%s] does not exist yet.", location) - result = location - break - - # Otherwise, confirm with the user that they want to overwrite the - # existing files or directories. If they don't want to, then loop - # back and prompt them for a new location. - else: - logger.debug( - "Location [%s] exists. Prompting for overwrite " - "permission.", - location, - ) - message = "Overwrite %s?" % location - title = "Project %s Exists" % title_type - action = confirm(parent_window, message, title) - if action == YES: - - # Only use the location if we successfully remove the - # existing files or directories at that location. - try: - self.model_service.clean_location(location) - result = location - break - - # Otherwise, display the remove error to the user and give - # them another chance to pick another location - except Exception as e: - msg = str(e) - title = "Unable To Overwrite %s" % location - information(parent_window, msg, title) - - logger.debug("Returning user location [%s]", result) - return result - - def _restore_from_autosave(self, project, autosave_loc): - """ Restores the project from the version saved in autosave_loc. - - """ - - workbench = self.model_service.application.get_service( - "envisage.ui.workbench.workbench.Workbench" - ) - window = workbench.active_window - app_name = workbench.branding.application_name - message = ( - "The app quit unexpectedly when [%s] was being modified.\n" - "An autosaved version of this project exists.\n" - "Do you want to restore the project from the " - "autosaved version ?" % project.name - ) - title = "%s-%s" % (app_name, project.name) - action = confirm( - window.control, message, title, cancel=True, default=YES - ) - if action == YES: - try: - saved_project = self.model_service.factory.open(autosave_loc) - if saved_project is not None: - # Copy over the autosaved version to the current project's - # location, switch the model service's project, and delete - # the autosaved version. - loc = project.location.strip() - saved_project.save(loc, overwrite=True) - self.model_service.clean_location(autosave_loc) - self.model_service.project = saved_project - else: - logger.debug( - "No usable project found in [%s]." % autosave_loc - ) - except: - logger.exception( - "Unable to restore project from [%s]" % autosave_loc - ) - self._start_timer(self.model_service.project) - - return - - def _save(self, project, parent_window, prompt_for_location=False): - """ - Save the specified project. If *prompt_for_location* is True, - or the project has no known location, then the user is prompted to - provide a location to save to. - - Returns True if the project was saved successfully, False if not. - - """ - - location = project.location.strip() - - # If the project's existing location is valid, check if there are any - # autosaved versions. - autosave_loc = "" - if location is not None and os.path.exists(location): - autosave_loc = self._get_autosave_location(location) - - # Ask the user to provide a location if we were told to do so or - # if the project has no existing location. - if prompt_for_location or location is None or len(location) < 1: - location = self._get_user_location(project, parent_window) - # Rename any existing autosaved versions to the new project - # location. - if location is not None and len(location) > 0: - self._clean_autosave_location(location) - new_autosave_loc = self._get_autosave_location(location) - if os.path.exists(autosave_loc): - shutil.move(autosave_loc, new_autosave_loc) - - # If we have a location to save to, try saving the project. - if location is not None and len(location) > 0: - try: - project.save(location) - saved = True - msg = '"%s" saved to %s' % (project.name, project.location) - information(parent_window, msg, "Project Saved") - logger.debug(msg) - - except Exception as e: - saved = False - logger.exception("Error saving project [%s]", project) - error(parent_window, str(e), title="Save Error") - else: - saved = False - - # If the save operation was successful, delete any autosaved files that - # exist. - if saved: - self._clean_autosave_location(location) - return saved - - def _show_open_dialog(self, parent): - """ - Show the dialog to open a project. - - """ - - # Determine the starting point for browsing. It is likely most - # projects will be stored in the default path used when creating new - # projects. - default_path = self.model_service.get_default_path() - project_class = self.model_service.factory.PROJECT_CLASS - - if self.model_service.are_projects_files(): - dialog = FileDialog( - parent=parent, - default_directory=default_path, - title="Open Project", - ) - if dialog.open() == OK: - path = dialog.path - else: - path = None - else: - dialog = DirectoryDialog( - parent=parent, - default_path=default_path, - message="Open Project", - ) - if dialog.open() == OK: - path = project_class.get_pickle_filename(dialog.path) - if File(path).exists: - path = dialog.path - else: - error( - parent, - "Directory does not contain a recognized " "project", - ) - path = None - else: - path = None - - return path - - def _start_timer(self, project): - """ - Resets the timer to work on auto-saving the current project. - - """ - - if self.timer is None: - if self.autosave_interval > 0: - # Timer needs the interval in millisecs - self.timer = Timer( - self.autosave_interval * 60000, self._auto_save, project - ) - return - - def _unbind_nodes(self, context, nodes): - """ - Unbinds all of the specified nodes that can be found within this - context or any of its sub-contexts. - - This uses a breadth first algorithm on the assumption that the - user will have likely selected peer nodes within a sub-context - that isn't the deepest context. - - """ - - logger.debug( - "Unbinding nodes [%s] from context [%s] within " "UiService [%s]", - nodes, - context, - self, - ) - - # Iterate through all of the selected nodes looking for ones who's - # name is within our context. - context_names = context.list_names() - for node in nodes[:]: - if node.name in context_names: - - # Ensure we've found a matching node by matching the objects - # as well. - binding = context.lookup_binding(node.name) - if id(node.obj) == id(binding.obj): - - # Remove the node from the context -AND- from the list of - # nodes that are still being searched for. - context.unbind(node.name) - nodes.remove(node) - - # Stop if we've unbound the last node - if len(nodes) < 1: - break - - # If we haven't unbound the last node, then search any sub-contexts - # for more nodes to unbind. - else: - - # Build a list of all current sub-contexts of this context. - subs = [] - for name in context.list_names(): - if context.is_context(name): - obj = context.lookup_binding(name).obj - sub_context = self._get_context_for_object(obj) - if sub_context is not None: - subs.append(sub_context) - - # Iterate through each sub-context, stopping as soon as possible - # if we've run out of nodes. - for sub in subs: - self._unbind_nodes(sub, nodes) - if len(nodes) < 1: - break - - def _workbench_exiting(self, event): - """ - Handle the workbench polling to see if it can exit and shutdown the - application. - - """ - - logger.debug("Detected workbench closing event in [%s]", self) - # Determine if the current project is dirty, or if an autosaved file - # exists for this project (i.e., the project has changes which were - # captured in the autosave operation but were not saved explicitly by - # the user). If so, let the user - # decide whether to veto the closing event, save the project, or - # ignore the dirty state. - current = self.model_service.project - - if not (self._get_project_state(current)): - # Find the active workbench window to be our dialog parent and - # the application name to use in our dialog title. - workbench = self.model_service.application.get_service( - "envisage.ui.workbench.workbench.Workbench" - ) - window = workbench.active_window - app_name = workbench.branding.application_name - - # Show a confirmation dialog to the user. - message = "Do you want to save changes before exiting?" - title = "%s - %s" % (current.name, app_name) - action = confirm( - window.control, message, title, cancel=True, default=YES - ) - if action == YES: - # If the save is successful, the autosaved file is deleted. - if not self._save(current, window.control): - event.veto = True - elif action == NO: - # Delete the autosaved file as the user does not wish to - # retain the unsaved changes. - self._clean_autosave_location(current.location.strip()) - elif action == CANCEL: - event.veto = True - - #### Trait change handlers ############################################### - - def _autosave_interval_changed(self, old, new): - """ - Restarts the timer when the autosave interval changes. - - """ - - self.timer = None - if new > 0 and self.model_service.project is not None: - self._start_timer(self.model_service.project) - return - - def _project_changed_for_model_service(self, object, name, old, new): - """ - Detects if an autosaved version exists for the project, and displays - a dialog to confirm restoring the project from the autosaved version. - - """ - - if old is not None: - self.timer = None - if new is not None: - # Check if an autosaved version exists and if so, display a dialog - # asking if the user wishes to restore the project from the - # autosaved version. - # Note: An autosaved version should exist only if the app crashed - # unexpectedly. Regular exiting of the workbench should cause the - # autosaved version to be deleted. - autosave_loc = self._get_autosave_location(new.location.strip()) - if os.path.exists(autosave_loc): - # Issue a do_later command here so as to allow time for the - # project view to be updated first to reflect the current - # project's state. - do_later(self._restore_from_autosave, new, autosave_loc) - else: - self._start_timer(new) - return diff --git a/envisage/ui/single_project/ui_service_factory.py b/envisage/ui/single_project/ui_service_factory.py deleted file mode 100644 index 9cde7dd69..000000000 --- a/envisage/ui/single_project/ui_service_factory.py +++ /dev/null @@ -1,41 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" -The default UI service factory. - -""" - - -# Enthought library imports. -from traits.api import HasTraits, Int, Str - -# Local imports. -from .ui_service import UiService - - -class UIServiceFactory(HasTraits): - """ - The default UI service factory. - - """ - - # The name of the class that implements the factory. - class_name = Str - - # The priority of this factory - priority = Int - - ########################################################################### - # 'UIServiceFactory' interface. - ########################################################################### - - def create_ui_service(self, *args, **kw): - """ Create the UI service. """ - - return UiService(*args, **kw) diff --git a/envisage/ui/single_project/view/__init__.py b/envisage/ui/single_project/view/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/envisage/ui/single_project/view/project_view.py b/envisage/ui/single_project/view/project_view.py deleted file mode 100644 index c9c0f8040..000000000 --- a/envisage/ui/single_project/view/project_view.py +++ /dev/null @@ -1,437 +0,0 @@ -# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX -# All rights reserved. -# -# This software is provided without warranty under the terms of the BSD -# license included in LICENSE.txt and may be redistributed only -# under the conditions described in the aforementioned license. The license -# is also available online at http://www.enthought.com/licenses/BSD.txt -# Thanks for using Enthought open source! -""" -The single project plugin's project view - -""" - -# Standard library imports. -import logging - -# Enthought library imports -from apptools.naming.api import Binding -from traits.api import register_factory, Any, HasTraits, Instance, Str -from traitsui.api import ( - Item, - Group, - TreeEditor, - ITreeNode, - ITreeNodeAdapter, - View, -) - -# Application specific imports. -from envisage.api import IApplication -from envisage.ui.single_project.project import Project -from envisage.ui.single_project.services import IPROJECT_MODEL, IPROJECT_UI - - -# Setup a logger for this module. -logger = logging.getLogger(__name__) - - -# Dummy EmptyProject class for when the ProjectView doesn't have a reference -# to a Project. -class EmptyProject(Project): - pass - - -class EmptyProjectAdapter(ITreeNodeAdapter): - """ Adapter for our EmptyProject. """ - - # -- ITreeNodeAdapter Method Overrides -------------------------------------- - - def get_label(self): - """ Gets the label to display for a specified object. - """ - return "No project loaded." - - -register_factory(EmptyProjectAdapter, EmptyProject, ITreeNode) - - -class ProjectAdapter(ITreeNodeAdapter): - """ Base ProjectAdapter for the root of the tree. """ - - # -- ITreeNodeAdapter Method Overrides -------------------------------------- - - def allows_children(self): - """ Returns whether this object can have children. - """ - return False - - def has_children(self): - """ Returns whether the object has children. - """ - return False - - def get_children(self): - """ Gets the object's children. - """ - return [] - - def get_label(self): - """ Gets the label to display for a specified object. - """ - return self.adaptee.name - - def get_tooltip(self): - """ Gets the tooltip to display for a specified object. - """ - return "Project" - - def get_icon(self, is_expanded): - """ Returns the icon for a specified object. - """ - return "" - - def can_auto_close(self): - """ Returns whether the object's children should be automatically - closed. - """ - return True - - -register_factory(ProjectAdapter, Project, ITreeNode) - - -class ProjectView(HasTraits): - """ - The single project plugin's project view - - """ - - ########################################################################## - # Traits - ########################################################################## - - #### public 'ProjectView' interface ###################################### - - # The Envisage application that this service is part of. - application = Instance(IApplication) - - # The suffix currently applied to our name - name_suffix = Str("") - - # The suffix applied to titles when the current project is dirty. - title_suffix = Str("*") - - # Root node for the project. - root = Instance(Project) - - # The traits view to display: - view = View( - Item( - "root", - editor=TreeEditor(editable=False, auto_open=1), - show_label=False, - ), - resizable=True, - ) - - ########################################################################## - # 'View' interface. - ########################################################################## - def __init__(self, **traits): - super().__init__(**traits) - - # Make sure our view stays in sync with the current project - model_service = self._get_model_service() - model_service.on_trait_change(self._on_project_changed, "project") - model_service.on_trait_change( - self._on_project_selection_changed, "selection" - ) - - # Make sure our control is initialized to the current project. - self._switch_projects(EmptyProject(), model_service.project) - - ########################################################################## - # 'ProjectView' interface. - ########################################################################## - - #### protected 'ProjectView' interface ################################# - - def _are_list_contents_different(self, list1, list2): - """ - Convenience method to determine if two lists contain different - items. Returns True if the lists are different and False - otherwise. - - """ - set1 = set(list1) - set2 = set(list2) - return set1 != set2 - - def _clear_state(self): - """ - Clears out all indications of any project state from this view. - - """ - - # self.name_suffix = '' - # Set the root to an EmptyProject. - self.root = EmptyProject() - - return - - def _get_model_service(self): - """ - Return a reference to the single project plugin's model service. - - """ - - return self.application.get_service(IPROJECT_MODEL) - - def _get_ui_service(self): - """ - Return a reference to the single project plugin's UI service. - - """ - - return self.window.application.get_service(IPROJECT_UI) - - def _switch_projects(self, old, new): - """ - Switches this view to the specified new project from the old project. - - Either value may be None. - - """ - - # Remove listeners on the old project, if any. - if old is not None and not isinstance(old, EmptyProject): - self._update_project_listeners(old, remove=True) - - # Update our state according to what the new project is, making sure - # to add listeners to any new project. - if new is not None: - logger.debug("Changing ProjectView to Project [%s]", new.name) - self._sync_state_to_project(new) - self._update_project_listeners(new) - else: - logger.debug("Changing ProjectView to no project") - self._clear_state() - - return - - def _sync_state_to_project(self, project): - """ - Sync the state of this view to the specified project's state. - - """ - - logger.debug( - "Syncing ProjectView [%s] to project state [%s]", self, project - ) - - # Update our Project reference. - self.root = project - - # Update our name suffix based on the dirty state of the project. - # self.name_suffix = (project.dirty and self.title_suffix) or '' - - return - - def _update_project_listeners(self, project, remove=False): - """ - Update listeners on the specified project instance. - - If remove is False then listeners are added, else listeners - are removed. - - """ - - project.on_trait_change(self._on_project_name_changed, "name", remove) - project.on_trait_change( - self._on_project_dirty_changed, "dirty", remove - ) - - return - - #### trait handlers ###################################################### - - def _name_suffix_changed(self, old, new): - """ - Handle changes to our name suffix. - - """ - - logger.debug( - "Detected change in name suffix to [%s] within " - + "ProjectView [%s]", - new, - self, - ) - - # Update our own name by removing the old suffix, if any, and adding - # on the new suffix, if any. - name = self.name - if old is not None and len(old) > 0: - index = (" " + old).rfind(name) - if index > -1: - name = name[0:index] - if new is not None and len(new) > 0: - name = name + " " + new - self.name = name - - # Update the containing window's suffix as well - self.window.title_suffix = new - - return - - def _on_control_right_clicked(self, event): - """ - Handle events when the tree control itself is right-clicked. - - """ - - logger.debug("ProjectView control right-clicked") - self._get_ui_service().display_default_context_menu( - self.control, event - ) - - return - - def _on_key_pressed_changed(self, event): - """ - Handle key presses while our control has focus. - - """ - - logger.debug("ProjectView key pressed [%s]", event) - - # If the delete key was pressed, then delete the current selected - # object. - if event.key_code == 127: - self._get_ui_service().delete_selection() - - return - - def _on_node_activated_changed(self, node): - """ - Handle nodes being activated (i.e. double-clicked.) - - """ - - logger.debug("ProjectView node activated [%s]", node) - - return - - def _on_project_changed(self, obj, trait_name, old, new): - """ - Handle when the current project changes. - - """ - - logger.debug("\n\n ***************** \n\n") - logger.debug( - "Detected project changed from [%s] to [%s] in " - "ProjectView [%s]", - old, - new, - self, - ) - - self._switch_projects(old, new) - - return - - def _on_project_dirty_changed(self, obj, trait_name, old, new): - """ - Handle the open project's dirty flag changing. - - """ - - logger.debug( - "Detected change in project dirty to [%s] within " - + "ProjectView [%s]", - new, - self, - ) - - return - - def _on_project_name_changed(self, obj, trait_name, old, new): - """ - Handle the open project's name changing. - - """ - - self._project_control.root.name = new - - return - - def _on_project_selection_changed(self, obj, trait_name, old, new): - """ - Handle the current project's selection changing. - - """ - - logger.debug( - "Detected project selection changed from [%s] " - + "to [%s] within ProjectView [%s]", - old, - new, - self, - ) - - # Ensure that the Tree control's selection matches the selection in - # the project. - control_selection = self._project_control.selection - if self._are_list_contents_different(new, control_selection): - logger.debug( - " Updating selection on tree control for " - + "ProjectView [%s]", - self, - ) - self._project_control.set_selection(new) - - # Ensure that this view's selection contains whatever was selected - # within the project. - if self._are_list_contents_different(new, self.selection): - logger.debug(" Updating selection on ProjectView [%s]", self) - self.selection = new - - return - - def _on_selection_changed(self, obj, trait_name, old, new): - """ - Handle selection changes in our tree control. - - """ - - logger.debug( - "Detected tree control selection change from [%s] " - + "to [%s] within ProjectView [%s]", - old, - new, - self, - ) - - # Ensure that the project model service's record of selection contains - # the same elements as the tree control. - model_service = self._get_model_service() - if self._are_list_contents_different(new, model_service.selection): - logger.debug(" Updating selection on project model service") - model_service.selection = new - - # Note that we don't have to update this view's selection record as - # we do that through our listener on the project selection. - - return - - def _on_closing_changed(self, old, new): - """ - Handle when this view closes. - - """ - - logger.debug("ProjectView [%s] closing!", self) - - return diff --git a/examples/plugins/single_project/sample_project/data/__init__.py b/examples/plugins/single_project/sample_project/data/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/plugins/single_project/sample_project/data/data.py b/examples/plugins/single_project/sample_project/data/data.py deleted file mode 100644 index 977781026..000000000 --- a/examples/plugins/single_project/sample_project/data/data.py +++ /dev/null @@ -1,231 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -# Enthought library imports. -from chaco.chaco_plot_editor import ChacoPlotItem -from blockcanvas.numerical_modeling.numeric_context.api import NumericContext -from traits.api import ( - adapts, - Array, - Enum, - Float, - HasTraits, - Instance, - Range, - Property, -) -from traitsui.api import ( - Group, - Item, - RangeEditor, - ITreeNode, - ITreeNodeAdapter, - View, -) -from numpy import arange - - -class DataView(HasTraits): - volume = Array - pressure = Property( - Array, depends_on=["temperature", "attraction", "totVolume"] - ) - attraction = Range(low=-50.0, high=50.0, value=0.0) - totVolume = Range(low=0.01, high=100.0, value=0.01) - temperature = Range(low=-50.0, high=50.0, value=50.0) - r_constant = Float(8.314472) - plot_type = Enum("line", "scatter") - - data_view = View( - ChacoPlotItem( - "volume", - "pressure", - type_trait="plot_type", - resizable=True, - x_label="Volume", - y_label="Pressure", - x_bounds=(-10, 120), - x_auto=False, - y_bounds=(-2000, 4000), - y_auto=False, - color="blue", - bgcolor="white", - border_visible=True, - border_width=1, - title="Pressure vs. Volume", - padding_bg_color="lightgray", - ), - Item(name="attraction"), - Item(name="totVolume"), - Item(name="temperature"), - Item(name="r_constant", style="readonly"), - Item(name="plot_type"), - resizable=True, - buttons=["OK"], - title="Van der waal Equation", - width=900, - height=500, - ) - - def _volume_default(self): - return arange(0.1, 100) - - # Pressure is calculated whenever one of the elements the property depends on changes. - def _get_pressure(self): - return ( - (self.r_constant * self.temperature) - / (self.volume - self.totVolume) - ) - (self.attraction / (self.volume * self.volume)) - - -class Data(NumericContext): - name = Property(depends_on=["context_name"]) - # data_parameters = Property - data_parameters = Instance(DataView) - - ################################################################################### - # Object Methods - ################################################################################### - """ Contains all of the data for a data """ - - def __init__(self, name="Unknown", **traits): - super().__init__(**traits) - self.context_name = name - self.data_parameters = DataView() - # self.data_parameters = DataView() - # self['data_parameters'] = DataParameters(self) - # TODO cgalvan: Init other data - - def __getstate__(self): - """ Return the state of this object for pickling. - - Extended to remove transient traits, and also store version - information. - - """ - - # Obtain state from base class(es) - state = super().__getstate__() - - # Add in our current version number. Note use a different attribute - # name from any base or derived class so that our numbers don't - # override theirs. - state["_data_version"] = 1 - - return state - - def __setstate__(self, state): - """ Restore the state of this object during unpickling. - - Extended to handle version upgrades. - - """ - - # Get the version info out of the state dictionary. - version = state.pop("_data_version", 0) - - # Upgrade to version 1. - if version < 1: - # Include dynamic bindings to all the numeric contexts in the - # dictionary - items_dict = {} - - if state.has_key("context_data"): - context_data = state["context_data"] - if isinstance(context_data, dict) and len(context_data) > 0: - items_dict = context_data._dict - - if len(items_dict) > 0: - self._add_all_items_as_dynamic_bindings_to_state( - state, items_dict - ) - - # Restore the base class's state. - super().__setstate__(state) - - return - - ############################################################################ - # Protected Methods - ############################################################################ - - # def _get_data_parameters(self): - # return self['data_parameters'] - - def _get_data_parameters(self): - return self.data_parameters - - def _get_name(self): - return self.context_name - - def _set_name(self, new_name): - self.context_name = new_name - return - - -class DataAdapter(ITreeNodeAdapter): - """ ITreeNodeAdapter for our custom Data object. """ - - adapts(Data, ITreeNode) - - # -- ITreeNodeAdapter Method Overrides -------------------------------------- - - def allows_children(self): - """ Returns whether this object can have children. - """ - return False - - def get_label(self): - """ Gets the label to display for a specified object. - """ - return self.adaptee.name - - def confirm_delete(self): - """ Checks whether a specified object can be deleted. - - Returns - ------- - * **True** if the object should be deleted with no further prompting. - * **False** if the object should not be deleted. - * Anything else: Caller should take its default action (which might - include prompting the user to confirm deletion). - """ - return None - - def when_label_changed(self, listener, remove): - """ Sets up or removes a listener for the label being changed on a - specified object. - """ - self.adaptee.on_trait_change( - listener, "list_items", remove=remove, dispatch="ui" - ) - - def get_tooltip(self): - """ Gets the tooltip to display for a specified object. - """ - return "Data" - - def get_icon(self, is_expanded): - """ Returns the icon for a specified object. - """ - return "" - - def can_auto_close(self): - """ Returns whether the object's children should be automatically - closed. - """ - return True - - def can_rename_me(self): - """ Returns whether the object can be renamed. - """ - return True - - def can_delete_me(self): - """ Returns whether the object can be deleted. - """ - return True diff --git a/examples/plugins/single_project/sample_project/data/plugin/__init__.py b/examples/plugins/single_project/sample_project/data/plugin/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/plugins/single_project/sample_project/data/plugin/action/__init__.py b/examples/plugins/single_project/sample_project/data/plugin/action/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/plugins/single_project/sample_project/data/plugin/action/data_plugin_action.py b/examples/plugins/single_project/sample_project/data/plugin/action/data_plugin_action.py deleted file mode 100644 index 72526874a..000000000 --- a/examples/plugins/single_project/sample_project/data/plugin/action/data_plugin_action.py +++ /dev/null @@ -1,171 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -A base class for actions that use the data plugin's services. - -""" - - -# Standard library imports. -import logging - -# Enthought library imports. -from envisage.api import UOL -from envisage.single_project.api import ProjectAction - -# Data library imports. -from data.data import Data - -# Application imports. -from data.plugin.services import IDATA_MODEL, IDATA_UI - - -# Create a logger for this module. -logger = logging.getLogger(__name__) - - -class DataPluginAction(ProjectAction): - """ - A base class for actions that use the data plugin's services. - - """ - - ########################################################################## - # Attributes - ########################################################################## - - #### public 'DataPluginAction' interface ################################# - - # A reference to the data plugin's model service. - data_model_service = UOL - - # A reference to the data plugin's UI service. - data_ui_service = UOL - - ########################################################################## - # 'object' interface - ########################################################################## - - #### operator methods #################################################### - - def __init__(self, **kws): - """ - Constructor. - - Extended to ensure that our UOL's have default identification strings. - - """ - - super().__init__(**kws) - - if self.data_model_service is None: - self.data_model_service = "service://%s" % IDATA_MODEL - - if self.data_ui_service is None: - self.data_ui_service = "service://%s" % IDATA_UI - - return - - ########################################################################## - # 'DataPluginAction' interface. - ########################################################################## - - #### protected methods ################################################### - - def _get_target_data(self, event, show_error=True): - """ - Return the data this action should import into. - - If the show_error parameter is True, then an error message will be - displayed to the user if a data could not be located. - - """ - - data = None - - # If the event has a node, then try to find the data from that node. - if hasattr(event, "node"): - data = self._get_target_data_from_node(event.node) - - # If we don't have a data yet, then try and find it from the current - # project selection. - if data is None: - data = self._get_target_data_from_project_selection() - - # If we have not identified a data, and we're supposed to show an error, - # then do so now. - if data is None and show_error: - self._display_error( - "Unable to identify a Data to perform this " - "action with. Please select a target Data within the project " - "tree and try again." - ) - - logger.debug("Target data: %s", data) - - return data - - def _get_target_data_from_node(self, node): - """ - Locate the data this action should import into from the specified node. - - """ - - # If the object itself is a data, then we're done - obj = node.obj - if isinstance(obj, Data): - return obj - - # Otherwise, scan upward through the tree containing the node, looking - # for the closest parent that is a data. - data = None - context = node.context - while context is not None: - if hasattr(context, "adaptee"): - obj = context.adaptee - if isinstance(obj, Data): - data = obj - break - if hasattr(context, "context"): - context = context.context - else: - context = None - - return data - - def _get_target_data_from_project_selection(self): - """ - Locate the data this action should import into from project selection. - - """ - - data = None - - # Find the first data in the project selection. - for item in self.model_service.selection: - if isinstance(item, Data): - data = item - break - elif hasattr(item, "obj") and isinstance(item.obj, Data): - data = item.obj - break - - return data - - def _get_node_name(self, event, show_error=True): - """ Return the name of the selection - """ - - if hasattr(event, "node"): - if hasattr(event.node, "name"): - return event.node.name - - if show_error: - logger.error("Could not retrieve the name of the selected data") - - return None diff --git a/examples/plugins/single_project/sample_project/data/plugin/action/edit_data_action.py b/examples/plugins/single_project/sample_project/data/plugin/action/edit_data_action.py deleted file mode 100644 index 18e9a50a5..000000000 --- a/examples/plugins/single_project/sample_project/data/plugin/action/edit_data_action.py +++ /dev/null @@ -1,53 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -An action to edit the data parameters of a Data. - -""" - - -# Standard imports. -import logging - -# Local imports -from data_plugin_action import DataPluginAction - - -# Create a logger for this module. -logger = logging.getLogger(__name__) - - -class EditDataAction(DataPluginAction): - """ - An action to edit the data parameters of a data. - - """ - - ########################################################################### - # 'Action' interface. - ########################################################################### - - #### public interface ##################################################### - - def perform(self, event): - """ - Perform this action. - - """ - - logger.debug("Performing EditDataAction [%s]", self) - - # Pull the parent for any displayed UI from the event. - window = event.window.control - - # If we can find a target data, then perform the operation. - data = self._get_target_data(event) - if data is not None: - self.data_ui_service.edit_data(window, data) - - return diff --git a/examples/plugins/single_project/sample_project/data/plugin/action/rename_data_action.py b/examples/plugins/single_project/sample_project/data/plugin/action/rename_data_action.py deleted file mode 100644 index 43fddee99..000000000 --- a/examples/plugins/single_project/sample_project/data/plugin/action/rename_data_action.py +++ /dev/null @@ -1,47 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -An action to rename a data. - -""" - - -# Standard imports. -import logging - -# Local imports -from data_plugin_action import DataPluginAction - - -# Create a logger for this module. -logger = logging.getLogger(__name__) - - -class RenameDataAction(DataPluginAction): - """ - An action to rename a data. - - """ - - ########################################################################### - # 'Action' interface. - ########################################################################### - - #### public interface ##################################################### - - def perform(self, event): - """ - Perform this action. - - """ - - logger.debug("Performing action [%s]", self) - - event.tree.edit_label(event.node) - - return diff --git a/examples/plugins/single_project/sample_project/data/plugin/data_action_set.py b/examples/plugins/single_project/sample_project/data/plugin/data_action_set.py deleted file mode 100644 index 5ba055112..000000000 --- a/examples/plugins/single_project/sample_project/data/plugin/data_action_set.py +++ /dev/null @@ -1,37 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -An action set for contributing actions to Data. - -""" - -# Enthought library imports. -from envisage.action.action_plugin_definition import ActionSet - - -############################################################################## -# Constants -############################################################################## - -# A commonly used string within our declarations. -ROOT = "data.plugin.resource_type" - -# The prefix used to identify menu-building resources, within an action set, -# for the context menu applying to a Data. This string MUST MATCH the string -# that the Data's node type is looking for! -DATA_CONTEXT_MENU = "%s.data_node_type" % ROOT - - -class DataActionSet(ActionSet): - """ - Action and menu definitions for the project view. - - """ - - # A mapping from human-readable root names to globally unique Ids. - aliases = {"DataContextMenu": DATA_CONTEXT_MENU} diff --git a/examples/plugins/single_project/sample_project/data/plugin/model_service.py b/examples/plugins/single_project/sample_project/data/plugin/model_service.py deleted file mode 100644 index 76403bea2..000000000 --- a/examples/plugins/single_project/sample_project/data/plugin/model_service.py +++ /dev/null @@ -1,63 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -The model service for the Data plugin. - -""" - -# Standard library imports. -import logging -import numpy - -# Enthought library imports. -from envisage.api import ApplicationObject -from apptools.naming.unique_name import make_unique_name -from blockcanvas.numerical_modeling.numeric_context.api import NumericContext -from blockcanvas.numerical_modeling.units.unit_array import UnitArray -from scimath.units.api import convert, unit_manager -from scimath.units.mass import gram -from scimath.units.volume import cc -from scimath.units.length import meter -from scimath.units.geo_units import ppg, psi -from pyface.wx.clipboard import clipboard - -# Data library imports. - - -# Setup a logger for this module -logger = logging.getLogger(__name__) - - -class ModelService(ApplicationObject): - """ - The model service for the Dataplugin. - - """ - - ########################################################################## - # 'ModelService' interface - ########################################################################## - - #### public methods ###################################################### - - def delete_context_item(self, context, item_name): - """ Deleting an item from a numeric context - - Parameters - ---------- - context : NumericContext - item_name : Str - - """ - - if isinstance(context, NumericContext) and context.has_key(item_name): - context.pop(item_name) - else: - logger.error("Invalid context or data not found in context") - - return diff --git a/examples/plugins/single_project/sample_project/data/plugin/plugin_definition.py b/examples/plugins/single_project/sample_project/data/plugin/plugin_definition.py deleted file mode 100644 index 89b6cb1ff..000000000 --- a/examples/plugins/single_project/sample_project/data/plugin/plugin_definition.py +++ /dev/null @@ -1,165 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -A Data resource type plugin. - -""" - -# Enthought library imports. -from envisage import PluginDefinition -from envisage.action.action_plugin_definition import Group, Location, Menu -from envisage.action.default_action import DefaultAction -from envisage.core.core_plugin_definition import ApplicationObject -from envisage.resource.resource_plugin_definition import ( - ResourceType, - ResourceManager, -) -from envisage.plugins.python_shell.python_shell_plugin_definition import ( - Namespace, -) - -# Local imports. -from data.plugin.services import IDATA_MODEL, IDATA_UI -from data.plugin.data_action_set import DataActionSet - - -############################################################################## -# Constants -############################################################################## - -# This plugin's globally unique identifier. Our usage's assume this is the -# python path to the package containing the plugin definition module. -ID = "data.plugin" - - -############################################################################## -# Extensions. -############################################################################## - -#### Actions and ActionSets ################################################## - - -class RenameDataAction(DefaultAction): - description = "Rename this data." - name = "&Rename" - - -class EditDataAction(DefaultAction): - description = "Edit data properties." - name = "&Edit Data Properties" - - -data_action_set = DataActionSet( - actions=[ - # # Data action group - # DeleteDataAction( - # locations = [ - # Location( - # after='RenameData', - # path='DataContextMenu/ActionGroup', - # ), - # ], - # ), - RenameDataAction( - locations=[Location(path="DataContextMenu/ActionGroup"),], - ), - EditDataAction( - locations=[Location(path="DataContextMenu/ActionGroup"),], - ), - ], - groups=[ - # Data groups - Group( - id="ActionGroup", - location=Location( - # after='PersistenceGroup', - path="DataContextMenu", - ), - ), - ], - id="%s.data_action_set.Default" % ID, - name="DataPlugin", -) - - -#### Application Objects ##################################################### - -model_service = ApplicationObject( - class_name="%s.model_service.ModelService" % ID, - uol="service://" + IDATA_MODEL, -) - -ui_service = ApplicationObject( - class_name="%s.ui_service.UiService" % ID, - kw={"model_service": model_service.uol}, - uol="service://" + IDATA_UI, -) - - -#### Resource Types ########################################################## - -# References to other plugin's resource types -FOLDER = "envisage.resource.folder_resource_type.FolderResourceType" -INSTANCE = "envisage.resource.instance_resource_type." "InstanceResourceType" - -# References to our resource types -DATA_TYPE = ID + ".resource_type.data_resource_type.DataResourceType" - -resource_types = ResourceManager( - resource_types=[ - ResourceType( - class_name=DATA_TYPE, - # precedes = [FOLDER, INSTANCE], - ), - ], -) - - -#### Shell Namespace ######################################################### - -# Import template code into the shell for scripting. -# namespace = Namespace( -# commands = [ -# 'from cp.data.api import *', -# ] -# ) - - -############################################################################## -# The plugin definition. -############################################################################## - - -class DataPlugin(PluginDefinition): - # The plugin's globally unique identifier. - id = ID - - # General information about the plugin. - name = "Data Plugin" - version = "0.0.1" - provider_name = "Enthought Inc" - provider_url = "www.enthought.com" - autostart = True - - # The Id's of the plugins that this plugin requires. - requires = [] - - # The extension points offered by this plugin. - extension_points = [ - DataActionSet, - ] - - # The contributions that this plugin makes to extension points offered by - # either itself or other plugins. - extensions = [ - model_service, - # namespace, - resource_types, - ui_service, - data_action_set, - ] diff --git a/examples/plugins/single_project/sample_project/data/plugin/services.py b/examples/plugins/single_project/sample_project/data/plugin/services.py deleted file mode 100644 index 88b613370..000000000 --- a/examples/plugins/single_project/sample_project/data/plugin/services.py +++ /dev/null @@ -1,17 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -The identifiers for the services in this plugin. - -""" - -# The model service identifier -IDATA_MODEL = "data.plugin.IModelService" - -# The UI service identifier. -IDATA_UI = "data.plugin.IUiService" diff --git a/examples/plugins/single_project/sample_project/data/plugin/ui_service.py b/examples/plugins/single_project/sample_project/data/plugin/ui_service.py deleted file mode 100644 index d2dc571d8..000000000 --- a/examples/plugins/single_project/sample_project/data/plugin/ui_service.py +++ /dev/null @@ -1,131 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -The UI service for the Data plugin. - -""" - -# Standard library imports. -import logging - -# Enthought library imports. -from envisage.api import ApplicationObject, UOL -from pyface.api import confirm, error, FileDialog, information, YES - -# Data library imports. - -# Local imports. -from services import IDATA_MODEL - - -# Setup a logger for this module -logger = logging.getLogger(__name__) - - -class UiService(ApplicationObject): - """ - The UI service for the Data plugin. - - """ - - ########################################################################## - # Attributes - ########################################################################## - - #### public 'UiService' interface ######################################## - - # A reference to the Data plugin's model service. - model_service = UOL - - ########################################################################## - # 'Object' interface - ########################################################################## - - #### operator methods #################################################### - - def __init__(self, **kws): - """ - Constructor. - - Extended to ensure our UOL properties are set. - - """ - - super().__init__(**kws) - - # Ensure we have a default model-service if one wasn't specified. - if self.model_service is None: - self.model_service = "service://%s" % IDATA_MODEL - - return - - ########################################################################## - # 'UIService' interface - ########################################################################## - - #### public methods ###################################################### - - # TODO cgalvan: to be implemented - # def delete_data(self, context, data_name, parent_window): - # """ - # Delete a Data. - # - # """ - # - # # Open confirmation-dialog to confirm deletion - # message = 'Are you sure you want to delete %s?' % data_name - # if confirm(parent_window, message) == YES: - # self.model_service.delete_context_item(context, data_name) - # - # return - - def edit_data(self, window, data): - """ - Edit the data parameters of the specified data. - - """ - - data_parameters = data.data_parameters - - edit_ui = data_parameters.edit_traits( - view="data_view", - kind="livemodal", - # handler=handler, - parent=window, - ) - - return edit_ui.result - - def display_message(self, msg, title=None, is_error=False): - """ - Display the specified message to the user. - - """ - - # Ensure we record any reasons this method doesn't work. Especially - # since it's critical in displaying errors to users! - try: - - # Attempt to identify the current application window. - parent_window = None - workbench = self.application.get_service( - "envisage." "workbench.IWorkbench" - ) - if workbench is not None: - parent_window = workbench.active_window.control - - # Display the requested message - if is_error: - error(parent_window, msg, title=title) - else: - information(parent_window, msg, title=title) - - except: - logger.exception("Unable to display pop-up message") - - return diff --git a/examples/plugins/single_project/sample_project/plugins/__init__.py b/examples/plugins/single_project/sample_project/plugins/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/plugins/single_project/sample_project/plugins/single_project/__init__.py b/examples/plugins/single_project/sample_project/plugins/single_project/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/plugins/single_project/sample_project/plugins/single_project/env_project.py b/examples/plugins/single_project/sample_project/plugins/single_project/env_project.py deleted file mode 100644 index ef2701f72..000000000 --- a/examples/plugins/single_project/sample_project/plugins/single_project/env_project.py +++ /dev/null @@ -1,296 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" - -Test envisage project. - -""" - -# System library imports. -import logging - -# Enthought library imports. -from envisage.ui.single_project.api import Project -from apptools.naming.unique_name import make_unique_name -from blockcanvas.numerical_modeling.numeric_context.numeric_context import ( - NumericContext, -) -from traits.api import adapts, property_depends_on, Property, List -from traitsui.api import Group, ITreeNode, ITreeNodeAdapter, View -from traitsui.menu import Action, Menu - -# Data import -from data.data import Data - -# Setup a logger for this module -logger = logging.getLogger(__name__) - - -class EnvProject(Project, NumericContext): - """ - Envisage project. - - """ - - ########################################################################## - # Attributes - ########################################################################## - - #### public 'Project' interface ########################################## - - # The location of this project on the filesystem. The default value - # depends on the runtime environment so we use a traits default method to - # set it. - # - # Overridden here to a File to use the File dialog since we're saving - # projects as files. - - # Set up the naming environment. - # klass_name = "apptools.naming.InitialContextFactory" - # environment = {Context.INITIAL_CONTEXT_FACTORY : klass_name} - # - # # Create an initial context. - # context = InitialContext(environment) - - # The list of names bound in this context: - list_names = Property(List) - - # The list of items bound in this context: - list_items = Property(List) - - # The UI view to use when creating a new project - traits_view = View( - Group("location"), - title="New Env Project", - id="plugins.single_project.env_project.EnvProject", - buttons=["OK", "Cancel"], - # Ensure closing via the dialog close button is the same - # as clicking cancel. - close_result=False, - # Ensure the user can resize the dialog. - resizable=True, - ) - - ########################################################################## - # 'object' interface - ########################################################################## - - #### operator methods #################################################### - - def __getstate__(self): - """ Return the state of this object for pickling. - - Extended to remove transient traits, and also store version - information. - - """ - - # Obtain state from base class(es) - state = super().__getstate__() - - # Add in our current version number. Note use a different attribute - # name from any base or derived class so that our numbers don't - # override theirs. - state["_env_project_version"] = 1 - - return state - - def __setstate__(self, state): - """ Restore the state of this object during unpickling. - - Extended to handle version upgrades. - - """ - - # Get the version info out of the state dictionary. - version = state.pop("_env_project_version", 0) - - # Upgrade to version 1. - if version < 1: - # Include dynamic bindings to all the numeric contexts in the - # dictionary - items_dict = {} - - if state.has_key("context_data"): - context_data = state["context_data"] - if isinstance(context_data, dict) and len(context_data) > 0: - items_dict = state["context_data"]._dict - - if len(items_dict) > 0: - self._add_all_items_as_dynamic_bindings_to_state( - state, items_dict - ) - - # Restore the base class's state. - super().__setstate__(state) - - return - - ########################################################################## - # 'EnvProject' interface - ########################################################################## - - #### property implementaions ############################################# - - @property_depends_on("dict_modified") - def _get_list_names(self): - """ - List the names bound in this context. - - """ - - result = [k for k, v in self.items() if isinstance(v, Data)] - result.sort() - return result - - @property_depends_on("dict_modified") - def _get_list_items(self): - """ - List the items bound in this context. - - """ - result = self.items() - result.sort(lambda l, r: cmp(l[0], r[0])) - return [v for n, v in result] - - def create_data(self, name="Data"): - """ - Create a new data within this project. - - The new data is initialized with the specified trait values and is - automatically added to this project. - - """ - - # Ensure the name we associate with the data within this project is - # unique. - name = make_unique_name(name, self.keys()) - - # Create the new data - data = Data(name) - - # Add it to ourself. - self.bind_dynamic(data, "context_name") - - logger.debug( - "Added new data (%s) to EnvProject (%s) with " "name (%s)", - data, - self, - name, - ) - - return data - - -class EnvProjectAdapter(ITreeNodeAdapter): - """ EnvProjectAdapter for our custom project. """ - - adapts(EnvProject, ITreeNode) - - # -- ITreeNodeAdapter Method Overrides -------------------------------------- - - def allows_children(self): - """ Returns whether this object can have children. - """ - return True - - def has_children(self): - """ Returns whether the object has children. - """ - return len(self.adaptee.list_items) > 0 - - def get_children(self): - """ Gets the object's children. - """ - return self.adaptee.list_items - - def get_children_id(self): - """ Gets the object's children identifier. - """ - return "list_items" - - def append_child(self, child=None): - """ Appends a child to the object's children. - """ - data = self.adaptee.create_data() - - def confirm_delete(self): - """ Checks whether a specified object can be deleted. - - Returns - ------- - * **True** if the object should be deleted with no further prompting. - * **False** if the object should not be deleted. - * Anything else: Caller should take its default action (which might - include prompting the user to confirm deletion). - """ - return False - - def delete_child(self, index): - """ Deletes a child at a specified index from the object's children. - """ - # Remove the child at the specified index. - child = self.adaptee.list_items[index] - self.adaptee._unbind_dynamic(child, "context_name") - - def when_children_replaced(self, listener, remove): - """ Sets up or removes a listener for children being replaced on a - specified object. - """ - self.adaptee.on_trait_change( - listener, "list_items", remove=remove, dispatch="ui" - ) - - def get_label(self): - """ Gets the label to display for a specified object. - """ - return self.adaptee.name - - def get_menu(self): - """ Returns the right-click context menu for an object. - """ - return Menu( - *[Action(name="Create Data", action="node.adapter.append_child",)] - ) - - def get_tooltip(self): - """ Gets the tooltip to display for a specified object. - """ - return "Project" - - def get_icon(self, is_expanded): - """ Returns the icon for a specified object. - """ - return "" - - def can_rename(self): - """ Returns whether the object's children can be renamed. - """ - return True - - def can_copy(self): - """ Returns whether the object's children can be copied. - """ - return True - - def can_delete(self): - """ Returns whether the object's children can be deleted. - """ - return True - - def can_auto_open(self): - """ Returns whether the object's children should be automatically - opened. - """ - return True - - def can_auto_close(self): - """ Returns whether the object's children should be automatically - closed. - """ - return False diff --git a/examples/plugins/single_project/sample_project/plugins/single_project/env_project_action_set.py b/examples/plugins/single_project/sample_project/plugins/single_project/env_project_action_set.py deleted file mode 100644 index ebce46c34..000000000 --- a/examples/plugins/single_project/sample_project/plugins/single_project/env_project_action_set.py +++ /dev/null @@ -1,42 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -An action set for contributing actions to components of a env -project. - -""" - - -# Enthought library imports. -from envisage.single_project.plugin_definition import ( - NO_SELECTION_MENU_ID, - PROJECT_MENU_ID, - SingleProjectActionSet, -) - - -############################################################################## -# Constants -############################################################################## - - -# A commonly used string within our declarations. -ROOT = "plugins.single_project.resource_type" - - -class EnvProjectActionSet(SingleProjectActionSet): - """ - Action and menu definitions for the project view. - - """ - - # A mapping from human-readable root names to globally unique Ids. - aliases = { - "NoSelectionMenu": NO_SELECTION_MENU_ID, - "ProjectMenu": PROJECT_MENU_ID, - } diff --git a/examples/plugins/single_project/sample_project/plugins/single_project/env_project_factory.py b/examples/plugins/single_project/sample_project/plugins/single_project/env_project_factory.py deleted file mode 100644 index 6df89f1e6..000000000 --- a/examples/plugins/single_project/sample_project/plugins/single_project/env_project_factory.py +++ /dev/null @@ -1,84 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -A customization of the single project factory to make EnvProjects. - -""" - -# Standard library imports -import logging - -# Enthought library imports. -from envisage.api import IApplication -from traits.api import Instance -from envisage.ui.single_project.api import ProjectFactory - -# Application imports. -from plugins.single_project.env_project import EnvProject - -# Setup a logger for this module. -logger = logging.getLogger(__name__) - - -class EnvProjectFactory(ProjectFactory): - """ - A customization of the single project factory to make EnvProjects. - - """ - - ########################################################################## - # Attributes - ########################################################################## - - #### public 'ProjectFactory' interface ################################### - - # The class of the project created by this factory. - # - # This is provided so that the single_project services can call class - # methods. - # - # This value is meant to be constant for the lifetime of this class! - PROJECT_CLASS = EnvProject - - # Current envisage application. - application = Instance(IApplication) - - #### public method ####################################################### - - def create(self): - """ - Create a new project from scratch. - - This must return an instance of a Project or 'None'. A return - value of 'None' indicates that no project could be created. The - plugin will display the default traits view to the user so that - they can configure this new project. - - """ - - return self.PROJECT_CLASS(application=self.application) - - def open(self, location): - """ - Open a project from the specified location. - - This must return an instance of a Project or 'None'. A return - value of 'None' indicates that no project could be opened from - the specified location. - - """ - - try: - project = self.PROJECT_CLASS.load(location, self.application) - except: - logger.exception( - "Unable to load Project from location %s", location - ) - project = None - - return project diff --git a/examples/plugins/single_project/sample_project/plugins/single_project/plugin_definition.py b/examples/plugins/single_project/sample_project/plugins/single_project/plugin_definition.py deleted file mode 100644 index f22c749ec..000000000 --- a/examples/plugins/single_project/sample_project/plugins/single_project/plugin_definition.py +++ /dev/null @@ -1,53 +0,0 @@ -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -The env application's extension of the single project plugin. - -""" - -# Standard imports -import os -import sys - -# Enthought library imports. -from envisage.api import Plugin, ServiceOffer -from envisage.ui.single_project.api import FactoryDefinition -from traits.api import List - -# This module's package. -PKG = ".".join(__name__.split(".")[:-1]) - -# Globally unique identifier. -ID = "plugins.single_project" - -############################################################################### -# `EnvProjectPlugin` class. -############################################################################### -class EnvProjectPlugin(Plugin): - - # Extension point Ids. - FACTORY_DEFINITIONS = "envisage.ui.single_project.factory_definitions" - - # The plugin's name. - name = "Env Project Plugin" - - ###### Contributions to extension points made by this plugin ###### - - # Factory definition we contribute to. - factory_definitions = List(contributes_to=FACTORY_DEFINITIONS) - - # Private methods. - def _factory_definitions_default(self): - """ Trait initializer. """ - factory_definition = FactoryDefinition( - class_name="%s.env_project_factory.EnvProjectFactory" % ID, - priority=10, - ) - return [factory_definition] - - # TODO: Add contributions project action set. diff --git a/examples/plugins/single_project/sample_project/run.py b/examples/plugins/single_project/sample_project/run.py deleted file mode 100644 index 2ca4586f4..000000000 --- a/examples/plugins/single_project/sample_project/run.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python - -# ----------------------------------------------------------------------------- -# -# Copyright (c) 2007 by Enthought, Inc. -# All rights reserved. -# -# ----------------------------------------------------------------------------- - -""" -The entry point for an Envisage application. - -""" - -# Standard library imports. -import logging - -# Enthought plugins. -from envisage.core_plugin import CorePlugin -from envisage.ui.workbench.workbench_plugin import WorkbenchPlugin -from envisage.ui.single_project.project_plugin import ProjectPlugin -from envisage.ui.workbench.api import WorkbenchApplication -from envisage.plugins.python_shell.python_shell_plugin import PythonShellPlugin - -# Local imports. -from plugins.single_project.plugin_definition import EnvProjectPlugin - -# FIXME: This is uncommented for now until we have the new TreeEditor -# implementation in place that can understand domain-objects that have -# been abstracted to an INode interface. -# from data.plugin.plugin_definition import DataPlugin - -# Configure a logger for this application -logger = logging.getLogger() -logger.addHandler(logging.StreamHandler()) -logger.setLevel(logging.DEBUG) - - -def main(): - """ Runs the application. """ - - # Create the application. - application = WorkbenchApplication( - id="testProject_extended", - plugins=[ - CorePlugin(), - WorkbenchPlugin(), - ProjectPlugin(), - EnvProjectPlugin(), - PythonShellPlugin(), - # FIXME: This is uncommented for now until we have the new TreeEditor - # implementation in place that can understand domain-objects that have - # been abstracted to an INode interface. - # DataPlugin(), - ], - ) - - # Run the application. - application.run() - - return - - -# Application entry point. -if __name__ == "__main__": - main() diff --git a/setup.cfg b/setup.cfg index 404c2bc15..aca1bfc9d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ [flake8] -exclude = examples, envisage/ui/single_project +exclude = examples ignore = E266, W503 per-file-ignores = */api.py:F401, setup.py:H102, diff --git a/setup.py b/setup.py index 22f3864d3..7a8ca2ad8 100644 --- a/setup.py +++ b/setup.py @@ -305,7 +305,6 @@ def get_long_description(): "plugins/banana/*.py", "plugins/orange/*.py", ], - "envisage.ui.single_project": ["*.txt"], "envisage.ui.tasks.tests": ["data/*.pkl"], }, python_requires=">=3.5",