Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

qtbot.mouseClick() not .click() #131

Draft
wants to merge 38 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
748f044
Add back dialogs
altendky Jun 18, 2020
5a49561
Merge branch 'master' into dialogs
altendky Jun 22, 2020
0790005
catch up with PySide2 support
altendky Jun 22, 2020
a0823de
Merge branch 'master' into dialogs
altendky Jun 22, 2020
d5c7980
Add some dialogs
altendky Jun 23, 2020
4160bae
allow FileDialog to not find buttons
altendky Jun 23, 2020
1e10b89
select the directory before the file, maybe
altendky Jun 23, 2020
29c2701
set the default path ahead for file dialog test
altendky Jun 23, 2020
70e8686
Merge branch 'master' into dialogs
altendky Jun 23, 2020
7c4ab04
actually do the last thing
altendky Jun 23, 2020
178ffa6
trio.Path() for test_file_save()
altendky Jun 23, 2020
fb5e377
maybe avoid a race and...
altendky Jun 23, 2020
9b4f7a1
Correct text input dialog shown signal parameter type
altendky Jun 23, 2020
1076979
Rework IntegerDialog to maybe avoid a race
altendky Jun 23, 2020
b28f094
Separate default directory and file
altendky Jun 23, 2020
4c7c1eb
non-native file dialog on macOS
altendky Jun 23, 2020
b3e3b30
hmm
altendky Jun 23, 2020
fce79ac
again
altendky Jun 23, 2020
bac2d5e
it's DontUseNativeDialog
altendky Jun 23, 2020
8aad207
rework test_wait_signal_waits
altendky Jun 24, 2020
f1d8b3a
Undo unrelated test changes
altendky Jul 14, 2020
b99b500
Merge branch 'master' into dialogs
altendky Jul 14, 2020
e96b61d
catchup
altendky Jul 15, 2020
8b7642f
diagnostic always uses non-native
altendky Jul 21, 2020
630ad35
Merge branch 'master' into dialogs
altendky Jul 24, 2020
ed14842
add newsfragments/2.feature.rst
altendky Jul 24, 2020
578a75f
Only non-native for macOS
altendky Jul 24, 2020
6cd7615
allow cancellation in the test
altendky Jul 24, 2020
234705a
give it more time?
altendky Jul 25, 2020
b233829
Merge branch 'master' into dialogs
altendky Jul 31, 2020
a06d76d
Raise timeout on failing tests
altendky Jul 31, 2020
8504f11
hmm
altendky Aug 1, 2020
ce60d1f
do we like classmethods?
altendky Aug 1, 2020
1f4966f
this will move elsewhere
altendky Aug 1, 2020
0670e89
move UserCancelledError back where it was
altendky Aug 1, 2020
983b564
Merge branch 'master' into dialogs
altendky Aug 3, 2020
4426bb4
Merge branch 'master' into dialogs
altendky Aug 3, 2020
84fa3b8
Switch back to qtbot.mouseClick()
altendky Aug 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/2.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introduce QTrio specific wrappers for some builtin dialogs.
1 change: 1 addition & 0 deletions qtrio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
EventTypeAlreadyRegisteredError,
ReturnCodeError,
RunnerTimedOutError,
UserCancelledError,
)

from ._core import (
Expand Down
339 changes: 339 additions & 0 deletions qtrio/_dialogs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
import contextlib
import os
import sys
import typing

import async_generator
import attr
from qtpy import QtCore
from qtpy import QtWidgets
import trio

import qtrio._qt


@contextlib.contextmanager
def manage(dialog):
finished_event = trio.Event()

def slot(*args, **kwargs):
if not finished_event.is_set():
finished_event.set()

with qtrio._qt.connection(signal=dialog.finished, slot=slot):
try:
dialog.setup()
yield finished_event
finally:
dialog.teardown()


@attr.s(auto_attribs=True)
class IntegerDialog:
parent: QtWidgets.QWidget
dialog: typing.Optional[QtWidgets.QInputDialog] = None
edit_widget: typing.Optional[QtWidgets.QWidget] = None
ok_button: typing.Optional[QtWidgets.QPushButton] = None
cancel_button: typing.Optional[QtWidgets.QPushButton] = None
attempt: typing.Optional[int] = None
result: typing.Optional[int] = None

shown = qtrio._qt.Signal(QtWidgets.QInputDialog)
hidden = qtrio._qt.Signal()
finished = qtrio._qt.Signal(int) # QtWidgets.QDialog.DialogCode

@classmethod
def build(cls, parent: QtCore.QObject = None,) -> "IntegerDialog":
return cls(parent=parent)

def setup(self):
self.result = None

self.dialog = QtWidgets.QInputDialog(self.parent)

# TODO: adjust so we can use a context manager?
self.dialog.finished.connect(self.finished)

self.dialog.show()

buttons = dialog_button_box_buttons_by_role(dialog=self.dialog)
self.ok_button = buttons.get(QtWidgets.QDialogButtonBox.AcceptRole)
self.cancel_button = buttons.get(QtWidgets.QDialogButtonBox.RejectRole)

[self.edit_widget] = self.dialog.findChildren(QtWidgets.QLineEdit)

if self.attempt is None:
self.attempt = 0
else:
self.attempt += 1

self.shown.emit(self.dialog)

def teardown(self):
if self.dialog is not None:
self.dialog.close()
self.dialog.finished.disconnect(self.finished)
self.dialog = None
self.ok_button = None
self.cancel_button = None
self.edit_widget = None

async def wait(self):
while True:
with manage(dialog=self) as finished_event:
await finished_event.wait()

if self.dialog.result() != QtWidgets.QDialog.Accepted:
raise qtrio.UserCancelledError()

try:
self.result = int(self.dialog.textValue())
except ValueError:
continue

return self.result


@attr.s(auto_attribs=True)
class TextInputDialog:
title: typing.Optional[str] = None
label: typing.Optional[str] = None
parent: typing.Optional[QtCore.QObject] = None

dialog: typing.Optional[QtWidgets.QInputDialog] = None
accept_button: typing.Optional[QtWidgets.QPushButton] = None
reject_button: typing.Optional[QtWidgets.QPushButton] = None
line_edit: typing.Optional[QtWidgets.QLineEdit] = None
result: typing.Optional[str] = None

shown = qtrio._qt.Signal(QtWidgets.QInputDialog)
finished = qtrio._qt.Signal(int) # QtWidgets.QDialog.DialogCode

@classmethod
def build(
cls,
title: typing.Optional[str] = None,
label: typing.Optional[str] = None,
parent: typing.Optional[QtCore.QObject] = None,
) -> "TextInputDialog":
return cls(title=title, label=label, parent=parent)

def setup(self):
self.result = None

self.dialog = QtWidgets.QInputDialog(parent=self.parent)
if self.label is not None:
self.dialog.setLabelText(self.label)
if self.title is not None:
self.dialog.setWindowTitle(self.title)

# TODO: adjust so we can use a context manager?
self.dialog.finished.connect(self.finished)
self.dialog.show()

buttons = dialog_button_box_buttons_by_role(dialog=self.dialog)
self.accept_button = buttons[QtWidgets.QDialogButtonBox.AcceptRole]
self.reject_button = buttons[QtWidgets.QDialogButtonBox.RejectRole]

[self.line_edit] = self.dialog.findChildren(QtWidgets.QLineEdit)

self.shown.emit(self.dialog)

def teardown(self):
if self.dialog is not None:
self.dialog.close()
self.dialog.finished.disconnect(self.finished)
self.dialog = None
self.accept_button = None
self.reject_button = None

async def wait(self):
with manage(dialog=self) as finished_event:
await finished_event.wait()

result = self.dialog.result()

if result == QtWidgets.QDialog.Rejected:
raise qtrio.UserCancelledError()

self.result = self.dialog.textValue()

return self.result


def dialog_button_box_buttons_by_role(
dialog: QtWidgets.QDialog,
) -> typing.Mapping[QtWidgets.QDialogButtonBox.ButtonRole, QtWidgets.QAbstractButton]:
hits = dialog.findChildren(QtWidgets.QDialogButtonBox)

if len(hits) == 0:
return {}

[button_box] = hits
return {button_box.buttonRole(button): button for button in button_box.buttons()}


@attr.s(auto_attribs=True)
class FileDialog:
file_mode: QtWidgets.QFileDialog.FileMode
accept_mode: QtWidgets.QFileDialog.AcceptMode
dialog: typing.Optional[QtWidgets.QFileDialog] = None
parent: typing.Optional[QtCore.QObject] = None
default_directory: typing.Optional[trio.Path] = None
default_file: typing.Optional[trio.Path] = None
options: QtWidgets.QFileDialog.Options = QtWidgets.QFileDialog.Options()
accept_button: typing.Optional[QtWidgets.QPushButton] = None
reject_button: typing.Optional[QtWidgets.QPushButton] = None
result: typing.Optional[trio.Path] = None

shown = qtrio._qt.Signal(QtWidgets.QFileDialog)
finished = qtrio._qt.Signal(int) # QtWidgets.QDialog.DialogCode

@classmethod
def build(
cls,
parent: typing.Optional[QtCore.QObject] = None,
default_directory: typing.Optional[trio.Path] = None,
default_file: typing.Optional[trio.Path] = None,
options: QtWidgets.QFileDialog.Options = QtWidgets.QFileDialog.Options(),
) -> "FileDialog":
return cls(
parent=parent,
default_directory=default_directory,
default_file=default_file,
options=options,
file_mode=QtWidgets.QFileDialog.AnyFile,
accept_mode=QtWidgets.QFileDialog.AcceptSave,
)

def setup(self):
self.result = None

extras = {}

if self.default_directory is not None:
extras["directory"] = os.fspath(self.default_directory)

options = self.options
if sys.platform == "darwin":
# https://github.com/altendky/qtrio/issues/28
options |= QtWidgets.QFileDialog.DontUseNativeDialog

self.dialog = QtWidgets.QFileDialog(
parent=self.parent, options=options, **extras
)

if self.default_file is not None:
self.dialog.selectFile(os.fspath(self.default_file))

self.dialog.setFileMode(self.file_mode)
self.dialog.setAcceptMode(self.accept_mode)

# TODO: adjust so we can use a context manager?
self.dialog.finished.connect(self.finished)

self.dialog.show()

buttons = dialog_button_box_buttons_by_role(dialog=self.dialog)
self.accept_button = buttons.get(QtWidgets.QDialogButtonBox.AcceptRole)
self.reject_button = buttons.get(QtWidgets.QDialogButtonBox.RejectRole)

self.shown.emit(self.dialog)

def teardown(self):
if self.dialog is not None:
self.dialog.close()
self.dialog.finished.disconnect(self.finished)
self.dialog = None
self.accept_button = None
self.reject_button = None

async def wait(self):
with manage(dialog=self) as finished_event:
await finished_event.wait()
if self.dialog.result() != QtWidgets.QDialog.Accepted:
self.result = None
else:
[path_string] = self.dialog.selectedFiles()
self.result = trio.Path(path_string)

return self.result


@attr.s(auto_attribs=True)
class MessageBox:
icon: QtWidgets.QMessageBox.Icon
title: str
text: str
buttons: QtWidgets.QMessageBox.StandardButtons

parent: typing.Optional[QtCore.QObject] = None

dialog: typing.Optional[QtWidgets.QMessageBox] = None
accept_button: typing.Optional[QtWidgets.QPushButton] = None
result: typing.Optional[trio.Path] = None

shown = qtrio._qt.Signal(QtWidgets.QMessageBox)
finished = qtrio._qt.Signal(int) # QtWidgets.QDialog.DialogCode

@classmethod
def build_information(
cls,
title: str,
text: str,
icon: QtWidgets.QMessageBox.Icon = QtWidgets.QMessageBox.Information,
buttons: QtWidgets.QMessageBox.StandardButtons = QtWidgets.QMessageBox.Ok,
parent: typing.Optional[QtCore.QObject] = None,
):
return cls(icon=icon, title=title, text=text, buttons=buttons, parent=parent)

def setup(self):
self.result = None

self.dialog = QtWidgets.QMessageBox(
self.icon, self.title, self.text, self.buttons, self.parent
)

# TODO: adjust so we can use a context manager?
self.dialog.finished.connect(self.finished)

self.dialog.show()

buttons = dialog_button_box_buttons_by_role(dialog=self.dialog)
self.accept_button = buttons[QtWidgets.QDialogButtonBox.AcceptRole]

self.shown.emit(self.dialog)

def teardown(self):
if self.dialog is not None:
self.dialog.close()
self.dialog = None
self.accept_button = None

async def wait(self):
with manage(dialog=self) as finished_event:
await finished_event.wait()

result = self.dialog.result()

if result == QtWidgets.QDialog.Rejected:
raise qtrio.UserCancelledError()


@async_generator.asynccontextmanager
async def manage_progress_dialog(
title: str,
label: str,
minimum: int = 0,
maximum: int = 0,
cancel_button_text: str = "Cancel",
parent: QtCore.QObject = None,
):
dialog = QtWidgets.QProgressDialog(
label, cancel_button_text, minimum, maximum, parent
)
try:
dialog.setWindowTitle(title)
yield dialog
finally:
dialog.close()
Loading