#!/usr/bin/env python
#
# dialogs.py
"""
Several dialog classes and helper functions for file/folder dialogs
"""
#
# Copyright 2019-2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
# stdlib
import os
from typing import List, Optional, Sequence
# 3rd party
import wx # type: ignore
# this package
from domdf_wxpython_tools.validators import CharValidator
__all__ = [
"file_dialog_wildcard",
"file_dialog_multiple",
"file_dialog",
"FloatEntryDialog",
"IntEntryDialog",
"Wildcards"
]
# style enumeration
style_uppercase = 4
style_lowercase = 8
style_hidden = 16
common_filetypes = {
"jpeg": ("JPEG files", ["jpg", "jpeg"]),
"png": ("PNG files", ["png"]),
"bmp": ("BMP files", ["bmp"]),
"tiff": ("TIFF files", ["tiff", "tif"]),
"gif": ("GIF files", ["gif"])
# TODO: Add more
}
[docs]def file_dialog_wildcard(
parent: wx.Window,
title: str,
wildcard: str,
style: int = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
**kwargs,
) -> Optional[List[str]]:
"""
Create a wx.FileDialog with the wildcard string given, and return a list of the files selected.
:param parent: Parent window. Should not be :py:obj:`None`.
:param wildcard:
:param title:
:param style:
:param kwargs:
:return: List of filenames for the selected files. If wx.FD_MULTIPLE is not in the style, the list will contain only one element
:rtype: list of str
"""
with wx.FileDialog(parent, title, wildcard=wildcard, style=style, **kwargs) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return None # the user changed their mind
try:
pathnames = fileDialog.GetPaths()
except:
pathnames = [fileDialog.GetPath()]
filter_extension_list = wildcard.split('|')[1::2]
valid_extensions = [os.path.splitext(ext)[1] for ext in ';'.join(filter_extension_list).split(';')]
selected_filter_index = fileDialog.GetFilterIndex()
selected_filter_extensions = filter_extension_list[selected_filter_index]
selected_filter_extensions = selected_filter_extensions.replace("*.", '.').split(';')
for index, pathname in enumerate(pathnames):
if selected_filter_extensions[0] != ".*":
if os.path.splitext(pathname)[-1].lower() not in selected_filter_extensions:
pathnames[index] = pathname + f"{selected_filter_extensions[0]}"
return pathnames
[docs]def file_dialog_multiple(
parent: wx.Window,
extension: str,
title: str,
filetypestring: str,
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
**kwargs
) -> Optional[List[str]]:
r"""
Create a :class:`wx.FileDialog` with the extension and filetypestring given,
and return a list of the files selected.
:param parent: Parent window. Should not be :py:obj:`None`.
:param extension:
:param title:
:param filetypestring:
:param style:
:param \*\*kwargs:
:return: List of filenames for the selected files.
If ``wx.FD_MULTIPLE`` is not in the style, the list will contain only one element.
"""
with wx.FileDialog(
parent,
title,
wildcard=f"{filetypestring} (*.{extension.lower()})|*.{extension.lower()};*.{extension.upper()}",
style=style,
**kwargs
) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return None # the user changed their mind
try:
pathnames = fileDialog.GetPaths()
except:
pathnames = [fileDialog.GetPath()]
for index, pathname in enumerate(pathnames):
if extension != '*':
if os.path.splitext(pathname)[-1].lower() != f".{extension}":
pathnames[index] = pathname + f".{extension}"
# else:
# pathnames[index] = os.path.splitext(pathname)[0]
return pathnames
[docs]def file_dialog(*args, **kwargs) -> Optional[str]:
r"""
Create a wx.FileDialog with for the extension and filetypestring given,
and return the filename selected.
:param parent:
:param extension:
:param title:
:param filetypestring:
:param style:
:param \*\*kwargs:
:return: The filename selected in the dialog.
If ``wx.FD_MULTIPLE`` is in the style, this will be the first filename selected.
"""
paths = file_dialog_multiple(*args, **kwargs)
if paths is not None:
return paths[0]
return None
[docs]class FloatEntryDialog(wx.TextEntryDialog):
"""
Alternative to wx.NumberEntryDialog that provides a TextCtrl which only allows numbers and decimal points to be entered.
Based on http://wxpython-users.1045709.n5.nabble.com/Adding-Validation-to-wx-TextEntryDialog-td2371082.html
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.textctrl = self.FindWindowById(3000)
self.textctrl.SetValidator(CharValidator("float-only"))
[docs] def GetValue(self):
value = self.textctrl.GetValue()
if value == '':
return None
else:
return float(value)
[docs]class IntEntryDialog(wx.TextEntryDialog):
"""
Alternative to wx.NumberEntryDialog that provides a TextCtrl which only allows numbers to be entered.
Based on http://wxpython-users.1045709.n5.nabble.com/Adding-Validation-to-wx-TextEntryDialog-td2371082.html
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.textctrl = self.FindWindowById(3000)
self.textctrl.SetValidator(CharValidator("int-only"))
[docs] def GetValue(self):
value = self.textctrl.GetValue()
if value == '':
return None
else:
return int(value)
[docs]class Wildcards:
"""
Class to generate glob wildcards for wx.FileDialog
"""
def __init__(self):
self._wildcards = []
[docs] def add_filetype(
self,
description: str,
extensions: Optional[Sequence[str]] = None,
hint_format: int = style_lowercase,
value_format: int = style_lowercase | style_uppercase
):
"""
Add a filetype to the wildcards
:param description: Description of the filetype
:param extensions: A list of valid file extensions for the filetype
:param hint_format: How the hints should be formatted.
:param value_format: How the values should be formatted.
Valid values for `hint_format` and `value_format` are `style_uppercase`,
`style_lowercase` and `style_hidden`, which can be combined using the `|` operator.
"""
if extensions:
hint = []
value = []
for extension in extensions:
if not extension.startswith("*."):
extension = f"*.{extension}"
# Hint
if not hint_format & style_hidden:
if hint_format & style_lowercase:
hint.append(extension.lower())
if hint_format & style_uppercase:
hint.append(extension.upper())
# Value
if value_format & style_lowercase:
value.append(extension.lower())
if value_format & style_uppercase:
value.append(extension.upper())
if hint_format & style_hidden:
self._wildcards.append(f"{description}|{';'.join(value)}")
else:
self._wildcards.append(f"{description} ({';'.join(hint)})|{';'.join(value)}")
else:
self._wildcards.append(f"{description}")
@property
def wildcard(self) -> str:
"""
Returns a string representing the wildcards for use in wx.FileDialog or file_dialog_wildcards
:rtype: str
"""
return '|'.join(self._wildcards)
[docs] def add_common_filetype(
self,
filetype: str,
hint_format: int = style_lowercase,
value_format: int = style_lowercase | style_uppercase,
):
"""
Add a common filetype.
:param filetype: The name of the filetype, Possible values are in common_filetypes
:param hint_format: How the hints should be formatted.
:param value_format: How the values should be formatted.
Valid values for `hint_format` and `value_format` are `style_uppercase`,
`style_lowercase` and `style_hidden`, which can be combined using the `|` operator.
"""
self.add_filetype(*common_filetypes[filetype], hint_format=hint_format, value_format=value_format)
[docs] def add_image_wildcard(self, value_format: int = style_lowercase | style_uppercase):
"""
Add a wildcard for all image filetypes.
:param value_format: How the values should be formatted.
Valid values for `value_format` are `style_uppercase`,
`style_lowercase` and `style_hidden`, which can be combined using the `|` operator.
"""
image_extensions = []
for key, item in common_filetypes.items():
if key in {"jpeg", "png", "bmp", "tiff", "gif"}:
for extension in item[1]:
image_extensions.append(f"*.{extension.lower()}")
image_extensions.append(f"*.{extension.upper()}")
self.add_filetype("Image files", image_extensions, hint_format=style_hidden, value_format=value_format)
[docs] def add_all_files_wildcard(self, hint_format: int = 0):
"""
Add a wildcard for 'All Files'.
:param hint_format: How the hints should be formatted. Valid values are :py:obj:`None` and `style_hidden`.
"""
if hint_format & style_hidden:
self._wildcards.append("All files|*.*")
else:
self._wildcards.append("All files (*.*)|*.*")
[docs] def __repr__(self):
return f"Wildcard = {self.wildcard}"
[docs] def __str__(self):
return self.wildcard
# legacy name
FileDialogWildcards = Wildcards