Skip to content

Commit

Permalink
Merge pull request #739 from mu-editor/remember-path
Browse files Browse the repository at this point in the history
File dialog will remember path last used.
  • Loading branch information
ntoll authored Jan 14, 2019
2 parents a794c84 + f2428b1 commit 5524e3e
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ matrix:

# To maximise compatibility pick earliest image, OS X 10.10 Yosemite
- os: osx
osx_image: xcode7.3
osx_image: xcode8
sudo: required
language: generic
python: 3.6
Expand Down
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ there are show-stoppers, the next release will be 1.1 with new features.
being truncated.
* Fix an off-by-one error when reading bytes from UART on MicroPython devices.
* Ensure zoom is consistent and remembered between panes and sessions.
* Ensure mu_code and/or current directory of current script are on Python path
in Mu installed from the installer on Windows. Thanks to Tim Golden and Tim
McCurrach for helping to test the fix.
* Added Argon, Boron and Xenon boards to Adafruit mode since they're also
supported by Adafruit's CircuitPython.
* The directory used to start a load/save dialog is either what the user last
selected, the current directory of the current file or the mode's working
directory (in order of precedence). This is reset when the mode is changed.
* Various minor typo and bug fixes.

1.0.1
Expand Down
33 changes: 29 additions & 4 deletions mu/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ def __init__(self, view, status_bar=None):
self.connected_devices = set()
self.find = ''
self.replace = ''
self.current_path = '' # Directory of last loaded file.
self.global_replace = False
self.selecting_mode = False # Flag to stop auto-detection of modes.
if not os.path.exists(DATA_DIR):
Expand Down Expand Up @@ -789,6 +790,27 @@ def _load(self, path):
self._view.add_tab(
name, text, self.modes[self.mode].api(), newline)

def get_dialog_directory(self):
"""
Return the directory folder in which a load/save dialog box should
open into. In order of precedence this function will return:
1) The last location used by a load/save dialog.
2) The directory containing the current file.
3) The mode's reported workspace directory.
"""
if self.current_path and os.path.isdir(self.current_path):
folder = self.current_path
else:
current_file_path = ''
workspace_path = self.modes[self.mode].workspace_dir()
tab = self._view.current_tab
if tab and tab.path:
current_file_path = os.path.dirname(os.path.abspath(tab.path))
folder = current_file_path if current_file_path else workspace_path
logger.info('Using path for file dialog: {}'.format(folder))
return folder

def load(self):
"""
Loads a Python file from the file system or extracts a Python script
Expand All @@ -802,9 +824,10 @@ def load(self):
extensions = set([e.lower() for e in extensions])
extensions = '*.{} *.{}'.format(' *.'.join(extensions),
' *.'.join(extensions).upper())
path = self._view.get_load_path(self.modes[self.mode].workspace_dir(),
extensions)
folder = self.get_dialog_directory()
path = self._view.get_load_path(folder, extensions)
if path:
self.current_path = os.path.dirname(os.path.abspath(path))
self._load(path)

def direct_load(self, path):
Expand Down Expand Up @@ -893,8 +916,8 @@ def save(self):
return
if not tab.path:
# Unsaved file.
workspace = self.modes[self.mode].workspace_dir()
path = self._view.get_save_path(workspace)
folder = self.get_dialog_directory()
path = self._view.get_save_path(folder)
if path and self.check_for_shadow_module(path):
message = _('You cannot use the filename '
'"{}"').format(os.path.basename(path))
Expand Down Expand Up @@ -1125,6 +1148,8 @@ def change_mode(self, mode):
# Update references to default file locations.
logger.info('Workspace directory: {}'.format(
self.modes[mode].workspace_dir()))
# Reset remembered current path for load/save dialogs.
self.current_path = ''
# Ensure auto-save timeouts are set.
if self.modes[mode].save_timeout > 0:
# Start the timer
Expand Down
2 changes: 1 addition & 1 deletion package/install_osx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ brew update >/dev/null 2>&1 # This produces a lot of output that's not very int

# Install Python 3.6
#brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/f2a764ef944b1080be64bd88dca9a1d80130c558/Formula/python.rb
brew upgrade pyenv
brew install pyenv
# The following are needed for Matplotlib
brew install freetype
120 changes: 119 additions & 1 deletion tests/test_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,14 +1019,14 @@ def test_no_duplicate_load_python_file():
editor_window.widgets = [unsaved_tab, brown_tab]

editor_window.get_load_path = mock.MagicMock(return_value=brown_script)
editor_window.current_tab.path = 'path'
# Create the "editor" that'll control the "window".
editor = mu.logic.Editor(view=editor_window)
mock_mode = mock.MagicMock()
mock_mode.workspace_dir.return_value = '/fake/path'
editor.modes = {
'python': mock_mode,
}

editor.load()
message = 'The file "{}" is already open.'.format(os.path.basename(
brown_script))
Expand All @@ -1043,6 +1043,7 @@ def test_load_other_file():
view.get_load_path = mock.MagicMock(return_value='foo.hex')
view.add_tab = mock.MagicMock()
view.show_confirmation = mock.MagicMock()
view.current_tab.path = 'path'
ed = mu.logic.Editor(view)
ed.change_mode = mock.MagicMock()
api = ['API specification', ]
Expand Down Expand Up @@ -1078,6 +1079,7 @@ def test_load_other_file_change_mode():
view.get_load_path = mock.MagicMock(return_value='foo.hex')
view.add_tab = mock.MagicMock()
view.show_confirmation = mock.MagicMock(return_value=QMessageBox.Ok)
view.current_tab.path = 'path'
ed = mu.logic.Editor(view)
ed.change_mode = mock.MagicMock()
api = ['API specification', ]
Expand Down Expand Up @@ -1114,6 +1116,7 @@ def test_load_other_file_with_exception():
view.get_load_path = mock.MagicMock(return_value='foo.hex')
view.add_tab = mock.MagicMock()
view.show_confirmation = mock.MagicMock()
view.current_tab.path = 'path'
ed = mu.logic.Editor(view)
ed.change_mode = mock.MagicMock()
mock_mb = mock.MagicMock()
Expand Down Expand Up @@ -1220,6 +1223,7 @@ def test_load_error():
view = mock.MagicMock()
view.get_load_path = mock.MagicMock(return_value='foo.py')
view.add_tab = mock.MagicMock()
view.current_tab.path = 'path'
ed = mu.logic.Editor(view)
mock_open = mock.MagicMock(side_effect=FileNotFoundError())
mock_mode = mock.MagicMock()
Expand All @@ -1233,6 +1237,120 @@ def test_load_error():
assert view.add_tab.call_count == 0


def test_load_sets_current_path():
"""
When a path has been selected for loading by the OS's file selector,
ensure that the directory containing the selected file is set as the
self.current_path for re-use later on.
"""
view = mock.MagicMock()
view.get_load_path = mock.MagicMock(return_value=os.path.join('path',
'foo.py'))
view.current_tab.path = os.path.join('old_path', 'foo.py')
ed = mu.logic.Editor(view)
ed._load = mock.MagicMock()
mock_mode = mock.MagicMock()
mock_mode.workspace_dir.return_value = '/fake/path'
mock_mode.file_extensions = ['html', 'css']
ed.modes = {
'python': mock_mode,
}
ed.load()
assert ed.current_path == os.path.abspath('path')


def test_load_no_current_path():
"""
If there is no self.current_path the default location to look for a file
to load is the directory containing the file currently being edited.
"""
view = mock.MagicMock()
view.get_load_path = mock.MagicMock(return_value=os.path.join('path',
'foo.py'))
view.current_tab.path = os.path.join('old_path', 'foo.py')
ed = mu.logic.Editor(view)
ed._load = mock.MagicMock()
mock_mode = mock.MagicMock()
mock_mode.workspace_dir.return_value = '/fake/path'
mock_mode.file_extensions = []
ed.modes = {
'python': mock_mode,
}
ed.load()
expected = os.path.abspath('old_path')
view.get_load_path.assert_called_once_with(expected, '*.py *.PY')


def test_load_no_current_path_no_current_tab():
"""
If there is no self.current_path nor is there a current file being edited
then the default location to look for a file to load is the current
mode's workspace directory. This used to be the default behaviour, but now
acts as a sensible fall-back.
"""
view = mock.MagicMock()
view.get_load_path = mock.MagicMock(return_value=os.path.join('path',
'foo.py'))
view.current_tab = None
ed = mu.logic.Editor(view)
ed._load = mock.MagicMock()
mock_mode = mock.MagicMock()
mock_mode.workspace_dir.return_value = os.path.join('fake', 'path')
mock_mode.file_extensions = []
ed.modes = {
'python': mock_mode,
}
ed.load()
expected = mock_mode.workspace_dir()
view.get_load_path.assert_called_once_with(expected, '*.py *.PY')


def test_load_has_current_path_does_not_exist():
"""
If there is a self.current_path but it doesn't exist, then use the expected
fallback as the location to look for a file to load.
"""
view = mock.MagicMock()
view.get_load_path = mock.MagicMock(return_value=os.path.join('path',
'foo.py'))
view.current_tab = None
ed = mu.logic.Editor(view)
ed._load = mock.MagicMock()
ed.current_path = 'foo'
mock_mode = mock.MagicMock()
mock_mode.workspace_dir.return_value = os.path.join('fake', 'path')
mock_mode.file_extensions = []
ed.modes = {
'python': mock_mode,
}
ed.load()
expected = mock_mode.workspace_dir()
view.get_load_path.assert_called_once_with(expected, '*.py *.PY')


def test_load_has_current_path():
"""
If there is a self.current_path then use this as the location to look for
a file to load.
"""
view = mock.MagicMock()
view.get_load_path = mock.MagicMock(return_value=os.path.join('path',
'foo.py'))
view.current_tab = None
ed = mu.logic.Editor(view)
ed._load = mock.MagicMock()
ed.current_path = 'foo'
mock_mode = mock.MagicMock()
mock_mode.workspace_dir.return_value = os.path.join('fake', 'path')
mock_mode.file_extensions = []
ed.modes = {
'python': mock_mode,
}
with mock.patch('os.path.isdir', return_value=True):
ed.load()
view.get_load_path.assert_called_once_with('foo', '*.py *.PY')


def test_check_for_shadow_module_with_match():
"""
If the name of the file in the path passed into check_for_shadow_module
Expand Down

0 comments on commit 5524e3e

Please sign in to comment.