On Sat, 14 Jan 2017 at 19:50:17 +0000, Simon McVittie wrote:
> > On Mon, Jan 9, 2017 at 2:21 PM, James Clarke <[email protected]> wrote:
> > > I would guess there are some UI
> > > interactions happening not on the main UI thread.
>
> I found a couple of places where this was done wrong. I'll send a
> patch series soon.
Try these?
They seem to work here, but I didn't get this crash myself (in GNOME
on Wayland with Gtk's standard Adwaita theme, if it matters) so I can't
say whether they are a complete fix.
S
>From 60718170c596a5daf38c84214941a69c3b05c897 Mon Sep 17 00:00:00 2001
From: James Clarke <[email protected]>
Date: Mon, 9 Jan 2017 18:48:43 +0000
Subject: [PATCH 1/6] gtk2_ui: Avoid deprecated Gtk.Expander(str) constructor
not exposed by gi
---
reportbug/ui/gtk2_ui.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/reportbug/ui/gtk2_ui.py b/reportbug/ui/gtk2_ui.py
index 2829cf0..e9099c2 100644
--- a/reportbug/ui/gtk2_ui.py
+++ b/reportbug/ui/gtk2_ui.py
@@ -191,7 +191,7 @@ class ExceptionDialog(CustomDialog):
def setup_dialog(self, vbox, tb):
# The traceback
- expander = Gtk.Expander("More details")
+ expander = Gtk.Expander.new_with_mnemonic("More details")
vbox.pack_start(expander, True, True, 0)
view = Gtk.TextView()
@@ -1170,7 +1170,7 @@ class EditorPage(Page):
scrolled = create_scrollable(self.view)
vbox.pack_start(scrolled, True, True, 0)
- expander = Gtk.Expander("Other system information")
+ expander = Gtk.Expander.new_with_mnemonic("Other system information")
view = Gtk.TextView()
view.set_editable(False)
self.others_buffer = view.get_buffer()
--
2.11.0
>From 533afffd7494d9257f89c4195d631b412a579a3f Mon Sep 17 00:00:00 2001
From: Simon McVittie <[email protected]>
Date: Sat, 14 Jan 2017 17:14:54 +0000
Subject: [PATCH 2/6] gtk2_ui: Consistently call set_progress_label in UI
thread
Previously, log_message() would call it from the reportbug thread.
---
reportbug/ui/gtk2_ui.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/reportbug/ui/gtk2_ui.py b/reportbug/ui/gtk2_ui.py
index e9099c2..0d3f14e 100644
--- a/reportbug/ui/gtk2_ui.py
+++ b/reportbug/ui/gtk2_ui.py
@@ -1448,6 +1448,7 @@ class ReportbugAssistant(Gtk.Assistant):
if self.showing_page == self.progress_page:
self.set_current_page(page.page_num)
+ # Called in UI thread
def set_progress_label(self, text, *args, **kwargs):
self.progress_page.set_label(text % args)
@@ -1520,8 +1521,9 @@ class GetFilenameDialog(ReportbugConnector, Gtk.FileChooserDialog):
self.show_all()
+# Called in reportbug thread
def log_message(*args, **kwargs):
- return assistant.set_progress_label(*args, **kwargs)
+ application.run_once_in_main_thread(assistant.set_progress_label, *args, **kwargs)
def select_multiple(*args, **kwargs):
--
2.11.0
>From 2294fc174ef4e6ca97c36b040700b10cf510cec0 Mon Sep 17 00:00:00 2001
From: Simon McVittie <[email protected]>
Date: Sat, 14 Jan 2017 18:32:11 +0000
Subject: [PATCH 3/6] gtk2_ui: Create a GLib main-context for each thread
A main context represents a set of callbacks, sockets and other
event sources, and each main-context can be acquired (owned) by at
most one thread at a time. A main-context is normally owned by the
same thread for its entire lifetime, and that's how we use them here.
The "thread-default main context" is how GLib and GIO track the thread
in which the callback for an async operation should be called; we don't
actually use any GIO async operations at the moment, but it's a good
idea to set it correctly. This also gives us the ability to give better
diagnostics than just asserting is_owner(), because we can show both
the expected thread and the actual thread in the assertion message.
---
reportbug/ui/gtk2_ui.py | 28 ++++++++++++++++++++++++++--
1 file changed, 26 insertions(+), 2 deletions(-)
diff --git a/reportbug/ui/gtk2_ui.py b/reportbug/ui/gtk2_ui.py
index 0d3f14e..87a8d79 100644
--- a/reportbug/ui/gtk2_ui.py
+++ b/reportbug/ui/gtk2_ui.py
@@ -71,7 +71,7 @@ from reportbug.urlutils import launch_browser
ISATTY = True
DEBIAN_LOGO = "/usr/share/pixmaps/debian-logo.png"
-global application, assistant, report_message
+global application, assistant, report_message, reportbug_context, ui_context
# Utilities
@@ -347,6 +347,7 @@ class BugPage(Gtk.EventBox, threading.Thread):
threading.Thread.__init__(self)
Gtk.EventBox.__init__(self)
self.setDaemon(True)
+ self.context = GLib.MainContext()
self.dialog = dialog
self.assistant = assistant
@@ -371,6 +372,11 @@ class BugPage(Gtk.EventBox, threading.Thread):
self.add(vbox)
def run(self):
+ if not self.context.acquire():
+ # should be impossible
+ raise AssertionError('Could not acquire my own main-context')
+ self.context.push_thread_default()
+
# Start the progress bar
GLib.timeout_add(10, self.pulse)
@@ -474,6 +480,7 @@ class BugsDialog(Gtk.Dialog):
# Application
class ReportbugApplication(threading.Thread):
def __init__(self):
+ _assert_context(reportbug_context)
threading.Thread.__init__(self)
self.setDaemon(True)
@@ -481,6 +488,11 @@ class ReportbugApplication(threading.Thread):
self.next_value = None
def run(self):
+ if not ui_context.acquire():
+ # should be impossible
+ raise AssertionError('Could not acquire UI context')
+ ui_context.push_thread_default()
+
Gdk.threads_enter()
Gtk.main()
Gdk.threads_leave()
@@ -1576,7 +1588,7 @@ def forward_operations(parent, operations):
def initialize():
- global application, assistant, Vte
+ global application, assistant, reportbug_context, ui_context, Vte
try:
gi.require_version('Vte', '2.91')
@@ -1595,6 +1607,18 @@ Falling back to 'text' interface."""
os.execlp('x-terminal-emulator', 'x-terminal-emulator', '-e', 'reportbug -u text')
return False
+ # The first thread of the process runs reportbug's UI-agnostic logic
+ reportbug_context = GLib.MainContext()
+ if not reportbug_context.acquire():
+ # should be impossible
+ raise AssertionError('Could not acquire new main-context')
+ reportbug_context.push_thread_default()
+
+ # A secondary thread (the ReportbugApplication) runs the GTK UI.
+ # This is the "default main context", used by GLib.idle_add() and similar
+ # non-thread-aware APIs.
+ ui_context = GLib.MainContext.default()
+
# Exception hook
oldhook = sys.excepthook
sys.excepthook = ExceptionDialog.create_excepthook(oldhook)
--
2.11.0
>From 65ef4b2e5623647a052329aa6a4c92e7895dd45a Mon Sep 17 00:00:00 2001
From: Simon McVittie <[email protected]>
Date: Sat, 14 Jan 2017 19:22:19 +0000
Subject: [PATCH 4/6] gtk2_ui: Simplify run_once_in_main_thread by inlining
create_idle_callback
---
reportbug/ui/gtk2_ui.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/reportbug/ui/gtk2_ui.py b/reportbug/ui/gtk2_ui.py
index 87a8d79..d4a84ba 100644
--- a/reportbug/ui/gtk2_ui.py
+++ b/reportbug/ui/gtk2_ui.py
@@ -507,15 +507,12 @@ class ReportbugApplication(threading.Thread):
def set_next_value(self, value):
self.next_value = value
- @staticmethod
- def create_idle_callback(func, *args, **kwargs):
+ def run_once_in_main_thread(self, func, *args, **kwargs):
def callback():
func(*args, **kwargs)
return False
- return callback
- def run_once_in_main_thread(self, func, *args, **kwargs):
- GLib.idle_add(self.create_idle_callback(func, *args, **kwargs))
+ GLib.idle_add(callback)
# Connection with reportbug
--
2.11.0
>From dbccc73fbbb6e999ac4609a12804a65eeebc56cd Mon Sep 17 00:00:00 2001
From: Simon McVittie <[email protected]>
Date: Sat, 14 Jan 2017 19:22:51 +0000
Subject: [PATCH 5/6] gtk2_ui: call GObject constructors in UI thread
Subclasses of Gtk objects are part of the Gtk API and must therefore
only be touched from the GLib "default main-context".
This means we have to rearrange initialization so we start the
main thread before we construct the rest of the objects.
---
reportbug/ui/gtk2_ui.py | 29 ++++++++++++++++++++++++-----
1 file changed, 24 insertions(+), 5 deletions(-)
diff --git a/reportbug/ui/gtk2_ui.py b/reportbug/ui/gtk2_ui.py
index d4a84ba..fbc4133 100644
--- a/reportbug/ui/gtk2_ui.py
+++ b/reportbug/ui/gtk2_ui.py
@@ -514,6 +514,26 @@ class ReportbugApplication(threading.Thread):
GLib.idle_add(callback)
+ def call_in_main_thread(self, func, *args, **kwargs):
+ def callback():
+ try:
+ ret = func(*args, **kwargs)
+ except BaseException as e:
+ self.set_next_value(e)
+ else:
+ self.set_next_value(ret)
+
+ self.put_next_value()
+ return False
+
+ GLib.idle_add(callback)
+ ret = self.get_last_value()
+
+ if isinstance(ret, BaseException):
+ raise ret
+ else:
+ return ret
+
# Connection with reportbug
# Syncronize "pipe" with reportbug
@@ -1569,7 +1589,7 @@ dialogs = {'yes_no': YesNoDialog,
def create_forwarder(parent, klass):
def func(*args, **kwargs):
- op = klass(parent)
+ op = application.call_in_main_thread(klass, parent)
try:
args, kwargs = op.sync_pre_operation(*args, **kwargs)
except SyncReturn as e:
@@ -1624,13 +1644,12 @@ Falling back to 'text' interface."""
Gtk.Window.set_default_icon_from_file(DEBIAN_LOGO)
application = ReportbugApplication()
- assistant = ReportbugAssistant(application)
+ application.start()
+ forward_operations(application, dialogs)
- # Forwarders
+ assistant = application.call_in_main_thread(ReportbugAssistant, application)
forward_operations(assistant, pages)
- forward_operations(application, dialogs)
- application.start()
return True
--
2.11.0
>From 7622be6b837a9792641419a55e483da89c4d2c98 Mon Sep 17 00:00:00 2001
From: Simon McVittie <[email protected]>
Date: Sat, 14 Jan 2017 19:32:50 +0000
Subject: [PATCH 6/6] gtk2_ui: Add assertions that we are in the intended
thread
---
reportbug/ui/gtk2_ui.py | 171 ++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 166 insertions(+), 5 deletions(-)
diff --git a/reportbug/ui/gtk2_ui.py b/reportbug/ui/gtk2_ui.py
index fbc4133..5a5e92c 100644
--- a/reportbug/ui/gtk2_ui.py
+++ b/reportbug/ui/gtk2_ui.py
@@ -76,6 +76,27 @@ global application, assistant, report_message, reportbug_context, ui_context
# Utilities
+def _describe_context(context):
+ if context == ui_context:
+ return '<MainContext of UI thread>'
+ elif context == reportbug_context:
+ return '<MainContext of reportbug thread>'
+ else:
+ return repr(context)
+
+
+def _assert_context(expected):
+ really = GLib.MainContext.ref_thread_default()
+
+ # This compares by pointer value of the underlying GMainContext
+ if really != expected:
+ raise AssertionError('Function should be called in %s but was called in %s' %
+ (_describe_context(really), _describe_context(expected)))
+
+ if not really.is_owner():
+ raise AssertionError('Function should be called with %s acquired')
+
+
def highlight(s):
return '<b>%s</b>' % s
@@ -94,6 +115,7 @@ def ask_free(s):
def create_scrollable(widget, with_viewport=False):
+ _assert_context(ui_context)
scrolled = Gtk.ScrolledWindow()
scrolled.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
@@ -105,6 +127,7 @@ def create_scrollable(widget, with_viewport=False):
def info_dialog(message):
+ _assert_context(ui_context)
dialog = Gtk.MessageDialog(assistant, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, message)
dialog.connect('response', lambda d, *args: d.destroy())
@@ -113,6 +136,7 @@ def info_dialog(message):
def error_dialog(message):
+ _assert_context(ui_context)
dialog = Gtk.MessageDialog(assistant, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, message)
dialog.connect('response', lambda d, *args: d.destroy())
@@ -122,6 +146,7 @@ def error_dialog(message):
class CustomDialog(Gtk.Dialog):
def __init__(self, stock_image, message, buttons, *args, **kwargs):
+ _assert_context(ui_context)
Gtk.Dialog.__init__(self, "Reportbug", assistant,
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
buttons)
@@ -155,15 +180,18 @@ class CustomDialog(Gtk.Dialog):
class InputStringDialog(CustomDialog):
def __init__(self, message):
+ _assert_context(ui_context)
CustomDialog.__init__(self, Gtk.STOCK_DIALOG_INFO, message,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT))
def setup_dialog(self, vbox):
+ _assert_context(ui_context)
self.entry = Gtk.Entry()
vbox.pack_start(self.entry, False, True, 0)
def get_value(self):
+ _assert_context(ui_context)
return self.entry.get_text()
@@ -171,7 +199,9 @@ class ExceptionDialog(CustomDialog):
# Register an exception hook to display an error when the GUI breaks
@classmethod
def create_excepthook(cls, oldhook):
+ _assert_context(reportbug_context)
def excepthook(exctype, value, tb):
+ # OK to call from any thread
if oldhook:
oldhook(exctype, value, tb)
application.run_once_in_main_thread(cls.start_dialog,
@@ -180,6 +210,7 @@ class ExceptionDialog(CustomDialog):
@classmethod
def start_dialog(cls, tb):
+ _assert_context(ui_context)
try:
dialog = cls(tb)
dialog.show_all()
@@ -187,6 +218,7 @@ class ExceptionDialog(CustomDialog):
sys.exit(1)
def __init__(self, tb):
+ _assert_context(ui_context)
CustomDialog.__init__(self, Gtk.STOCK_DIALOG_ERROR, "An error has occurred while doing an operation in Reportbug.\nPlease report the bug.", (Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE), tb)
def setup_dialog(self, vbox, tb):
@@ -203,11 +235,13 @@ class ExceptionDialog(CustomDialog):
self.connect('response', self.on_response)
def on_response(self, dialog, res):
+ _assert_context(ui_context)
sys.exit(1)
class ReportViewerDialog(Gtk.Dialog):
def __init__(self, message):
+ _assert_context(ui_context)
Gtk.Dialog.__init__(self, "Reportbug", assistant,
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
(Gtk.STOCK_COPY, Gtk.ResponseType.APPLY,
@@ -226,6 +260,7 @@ class ReportViewerDialog(Gtk.Dialog):
self.show_all()
def on_response(self, dialog, res):
+ _assert_context(ui_context)
# ok Gtk.ResponseType.APPLY is ugly for Gtk.STOCK_COPY, but who cares?
# maybe adding it as a secondary button or such is better
if res == Gtk.ResponseType.APPLY:
@@ -344,6 +379,7 @@ class BugReport(object):
# BTS GUI
class BugPage(Gtk.EventBox, threading.Thread):
def __init__(self, assistant, dialog, number, queryonly, bts, mirrors, http_proxy, timeout, archived):
+ _assert_context(ui_context)
threading.Thread.__init__(self)
Gtk.EventBox.__init__(self)
self.setDaemon(True)
@@ -390,21 +426,25 @@ class BugPage(Gtk.EventBox, threading.Thread):
self.application.run_once_in_main_thread(self.found, info)
def drop_progressbar(self):
+ _assert_context(ui_context)
child = self.get_child()
if child:
self.remove(child)
child.unparent()
def pulse(self):
+ _assert_context(ui_context)
self.progress.pulse()
return self.isAlive()
def not_found(self):
+ _assert_context(ui_context)
self.drop_progressbar()
self.add(Gtk.Label("The bug can't be fetched or it doesn't exist."))
self.show_all()
def found(self, info):
+ _assert_context(ui_context)
self.drop_progressbar()
desc = info[0].subject
bodies = info[1]
@@ -443,9 +483,11 @@ class BugPage(Gtk.EventBox, threading.Thread):
self.show_all()
def on_open_browser(self, button):
+ _assert_context(ui_context)
launch_browser(debbugs.get_report_url(self.bts, int(self.number), self.archived))
def on_reply(self, button):
+ _assert_context(ui_context)
# Return the bug number to reportbug
self.application.set_next_value(self.bug_status)
# Forward the assistant to the progress bar
@@ -457,6 +499,7 @@ class BugPage(Gtk.EventBox, threading.Thread):
class BugsDialog(Gtk.Dialog):
def __init__(self, assistant, queryonly):
+ _assert_context(ui_context)
Gtk.Dialog.__init__(self, "Reportbug: bug information", assistant,
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE))
@@ -498,24 +541,33 @@ class ReportbugApplication(threading.Thread):
Gdk.threads_leave()
def get_last_value(self):
+ _assert_context(reportbug_context)
return self.queue.get()
def put_next_value(self):
+ _assert_context(ui_context)
self.queue.put(self.next_value)
self.next_value = None
def set_next_value(self, value):
+ _assert_context(ui_context)
self.next_value = value
def run_once_in_main_thread(self, func, *args, **kwargs):
+ # OK to call from any thread
+
def callback():
+ _assert_context(ui_context)
func(*args, **kwargs)
return False
GLib.idle_add(callback)
def call_in_main_thread(self, func, *args, **kwargs):
+ # OK to call from any thread
+
def callback():
+ _assert_context(ui_context)
try:
ret = func(*args, **kwargs)
except BaseException as e:
@@ -539,18 +591,20 @@ class ReportbugApplication(threading.Thread):
# Syncronize "pipe" with reportbug
class SyncReturn(RuntimeError):
def __init__(self, result):
+ _assert_context(reportbug_context)
RuntimeError.__init__(self, result)
self.result = result
class ReportbugConnector(object):
- # Executed in the glib thread
def execute_operation(self, *args, **kwargs):
+ _assert_context(ui_context)
pass
# Executed in sync with reportbug. raise SyncResult(value) to directly return to reportbug
# Returns args and kwargs to pass to execute_operation
def sync_pre_operation(cls, *args, **kwargs):
+ _assert_context(reportbug_context)
return args, kwargs
@@ -563,6 +617,7 @@ class Page(ReportbugConnector):
WARNING_COLOR = Gdk.color_parse("#fff8ae")
def __init__(self, assistant):
+ _assert_context(ui_context)
self.assistant = assistant
self.application = assistant.application
self.widget = self.create_widget()
@@ -572,6 +627,7 @@ class Page(ReportbugConnector):
self.page_num = Page.next_page_num
def execute_operation(self, *args, **kwargs):
+ _assert_context(ui_context)
self.switch_in()
self.connect_signals()
self.empty_ok = kwargs.pop('empty_ok', False)
@@ -581,20 +637,24 @@ class Page(ReportbugConnector):
self.setup_focus()
def connect_signals(self):
- pass
+ _assert_context(ui_context)
def set_page_complete(self, complete):
+ _assert_context(ui_context)
self.assistant.set_page_complete(self.widget, complete)
def set_page_type(self, type):
+ _assert_context(ui_context)
self.assistant.set_page_type(self.widget, type)
def set_page_title(self, title):
+ _assert_context(ui_context)
if title:
self.assistant.set_page_title(self.widget, title)
# The user will see this as next page
def switch_in(self):
+ _assert_context(ui_context)
Page.next_page_num += 1
self.assistant.insert_page(self.widget, self.page_num)
self.set_page_complete(self.default_complete)
@@ -605,24 +665,30 @@ class Page(ReportbugConnector):
# Setup keyboard focus in the page
def setup_focus(self):
+ _assert_context(ui_context)
self.widget.grab_focus()
# Forward page when a widget is activated(e.g. GtkEntry) only if page is complete
def activate_forward(self, *args):
+ _assert_context(ui_context)
if self.assistant.get_page_complete(self.widget):
self.assistant.forward_page()
# The user forwarded the assistant to see the next page
def switch_out(self):
- pass
+ _assert_context(ui_context)
def is_valid(self, value):
+ _assert_context(ui_context)
+
if self.empty_ok:
return True
else:
return bool(value)
def validate(self, *args, **kwargs):
+ _assert_context(ui_context)
+
value = self.get_value()
if self.is_valid(value):
self.application.set_next_value(value)
@@ -636,6 +702,8 @@ class IntroPage(Page):
default_complete = True
def create_widget(self):
+ _assert_context(ui_context)
+
vbox = Gtk.VBox(spacing=24)
label = Gtk.Label("""
@@ -657,9 +725,11 @@ This wizard will guide you through the bug reporting process step by step.
class GetStringPage(Page):
def setup_focus(self):
+ _assert_context(ui_context)
self.entry.grab_focus()
def create_widget(self):
+ _assert_context(ui_context)
vbox = Gtk.VBox(spacing=12)
self.label = Gtk.Label()
self.label.set_line_wrap(True)
@@ -672,13 +742,16 @@ class GetStringPage(Page):
return vbox
def connect_signals(self):
+ _assert_context(ui_context)
self.entry.connect('changed', self.validate)
self.entry.connect('activate', self.activate_forward)
def get_value(self):
+ _assert_context(ui_context)
return self.entry.get_text()
def execute(self, prompt, options=None, force_prompt=False, default=''):
+ _assert_context(ui_context)
# Hackish: remove the text needed for textual UIs...
GLib.idle_add(self.label.set_text, prompt.replace('(enter Ctrl+c to exit reportbug without reporting a bug)', ''))
self.entry.set_text(default)
@@ -701,6 +774,7 @@ class GetStringPage(Page):
class GetPasswordPage(GetStringPage):
def create_widget(self):
+ _assert_context(ui_context)
widget = GetStringPage.create_widget(self)
self.entry.set_visibility(False)
return widget
@@ -708,9 +782,11 @@ class GetPasswordPage(GetStringPage):
class GetMultilinePage(Page):
def setup_focus(self):
+ _assert_context(ui_context)
self.view.grab_focus()
def create_widget(self):
+ _assert_context(ui_context)
vbox = Gtk.VBox(spacing=12)
self.label = Gtk.Label()
self.label.set_line_wrap(True)
@@ -726,9 +802,11 @@ class GetMultilinePage(Page):
return vbox
def connect_signals(self):
+ _assert_context(ui_context)
self.buffer.connect('changed', self.validate)
def get_value(self):
+ _assert_context(ui_context)
text = self.buffer.get_text(self.buffer.get_start_iter(), self.buffer.get_end_iter())
lines = text.split('\n')
# Remove the trailing empty line at the end
@@ -737,6 +815,7 @@ class GetMultilinePage(Page):
return text.split('\n')
def execute(self, prompt):
+ _assert_context(ui_context)
self.empty_ok = True
# The result must be iterable for reportbug even if it's empty and not modified
GLib.idle_add(self.label.set_text, prompt)
@@ -748,16 +827,20 @@ class TreePage(Page):
value_column = None
def __init__(self, *args, **kwargs):
+ _assert_context(ui_context)
Page.__init__(self, *args, **kwargs)
self.selection = self.view.get_selection()
def setup_focus(self):
+ _assert_context(ui_context)
self.view.grab_focus()
def connect_signals(self):
+ _assert_context(ui_context)
self.selection.connect('changed', self.validate)
def get_value(self):
+ _assert_context(ui_context)
model, paths = self.selection.get_selected_rows()
multiple = self.selection.get_mode() == Gtk.SelectionMode.MULTIPLE
result = []
@@ -774,6 +857,7 @@ class GetListPage(TreePage):
value_column = 0
def create_widget(self):
+ _assert_context(ui_context)
vbox = Gtk.VBox(spacing=12)
self.label = Gtk.Label()
self.label.set_line_wrap(True)
@@ -803,22 +887,26 @@ class GetListPage(TreePage):
return vbox
def get_value(self):
+ _assert_context(ui_context)
values = []
for row in self.model:
values.append(row[self.value_column])
return values
def on_add(self, button):
+ _assert_context(ui_context)
dialog = InputStringDialog("Add a new item to the list")
dialog.show_all()
dialog.connect('response', self.on_add_dialog_response)
def on_add_dialog_response(self, dialog, res):
+ _assert_context(ui_context)
if res == Gtk.ResponseType.ACCEPT:
self.model.append([dialog.get_value()])
dialog.destroy()
def on_remove(self, button):
+ _assert_context(ui_context)
model, paths = self.selection.get_selected_rows()
# We need to transform them to iters, since paths change when removing rows
iters = []
@@ -828,6 +916,7 @@ class GetListPage(TreePage):
self.model.remove(iter)
def execute(self, prompt):
+ _assert_context(ui_context)
self.empty_ok = True
GLib.idle_add(self.label.set_text, prompt)
@@ -843,6 +932,7 @@ class GetListPage(TreePage):
class WrapRendererText(Gtk.CellRendererText):
def do_render(self, cr, widget, background_area, cell_area, flags):
+ _assert_context(ui_context)
self.set_property('wrap-width', cell_area.width)
Gtk.CellRendererText.do_render(self, cr, widget, background_area, cell_area, flags)
@@ -854,6 +944,7 @@ class MenuPage(TreePage):
value_column = 0
def create_widget(self):
+ _assert_context(ui_context)
vbox = Gtk.VBox(spacing=12)
self.label = Gtk.Label()
self.label.set_line_wrap(True)
@@ -869,11 +960,13 @@ class MenuPage(TreePage):
return vbox
def connect_signals(self):
+ _assert_context(ui_context)
TreePage.connect_signals(self)
self.view.connect('row-activated', self.activate_forward)
def execute(self, par, options, prompt, default=None, any_ok=False,
order=None, extras=None, multiple=False):
+ _assert_context(ui_context)
GLib.idle_add(self.label.set_text, par)
self.model = Gtk.ListStore(str, str)
@@ -923,6 +1016,7 @@ class HandleBTSQueryPage(TreePage):
def sync_pre_operation(self, package, bts, timeout, mirrors=None, http_proxy="", queryonly=False, screen=None,
archived='no', source=False, title=None,
version=None, buglist=None, mbox_reader_cmd=None, latest_first=False):
+ _assert_context(reportbug_context)
self.bts = bts
self.mirrors = mirrors
self.http_proxy = http_proxy
@@ -989,9 +1083,11 @@ class HandleBTSQueryPage(TreePage):
raise SyncReturn(None)
def setup_focus(self):
+ _assert_context(ui_context)
self.entry.grab_focus()
def create_widget(self):
+ _assert_context(ui_context)
vbox = Gtk.VBox(spacing=6)
self.label = Gtk.Label("List of bugs. Select a bug to retrieve and submit more information.")
vbox.pack_start(self.label, False, True, 6)
@@ -1026,18 +1122,22 @@ class HandleBTSQueryPage(TreePage):
return vbox
def connect_signals(self):
+ _assert_context(ui_context)
TreePage.connect_signals(self)
self.view.connect('row-activated', self.on_retrieve_info)
self.entry.connect('changed', self.on_filter_changed)
def on_filter_clear(self, button):
+ _assert_context(ui_context)
self.entry.set_text("")
def on_filter_changed(self, entry):
+ _assert_context(ui_context)
self.model.filter_text = entry.get_text().lower()
self.filter.refilter()
def on_retrieve_info(self, *args):
+ _assert_context(ui_context)
bug_ids = TreePage.get_value(self)
if not bug_ids:
info_dialog("Please select one ore more bugs")
@@ -1049,13 +1149,16 @@ class HandleBTSQueryPage(TreePage):
dialog.show_all()
def is_valid(self, value):
+ _assert_context(ui_context)
return True
def get_value(self):
+ _assert_context(ui_context)
# The value returned to reportbug doesn't depend by a selection, but by the dialog of a bug
return None
def match_filter(self, iter):
+ _assert_context(ui_context)
# Flatten the columns into a single string
text = ""
for col in range(len(self.columns)):
@@ -1071,6 +1174,7 @@ class HandleBTSQueryPage(TreePage):
return False
def filter_visible_func(self, model, iter, user_data=None):
+ _assert_context(ui_context)
matches = self.match_filter(iter)
if not self.model.iter_parent(iter) and not matches:
# If no children are visible, hide it
@@ -1084,6 +1188,7 @@ class HandleBTSQueryPage(TreePage):
return matches
def execute(self, buglist, sectitle):
+ _assert_context(ui_context)
GLib.idle_add(self.label.set_text, "%s. Double-click a bug to retrieve and submit more information." % sectitle)
self.model = Gtk.TreeStore(*([str] * len(self.columns)))
@@ -1106,21 +1211,26 @@ class ShowReportPage(Page):
default_complete = True
def create_widget(self):
+ _assert_context(ui_context)
self.page = BugPage(self.assistant, None, None, None, None, None, None, None, None)
return self.page
def get_value(self):
+ _assert_context(ui_context)
return None
def is_valid(self, value):
+ _assert_context(ui_context)
return True
def sync_pre_operation(self, *args, **kwargs):
+ _assert_context(reportbug_context)
if kwargs.get('queryonly'):
self.page_type = Gtk.AssistantPageType.CONFIRM
return args, kwargs
def execute(self, number, system, mirrors, http_proxy, timeout, queryonly=False, title='', archived='no', mbox_reader_cmd=None):
+ _assert_context(ui_context)
self.page.number = number
self.page.bts = system
self.page.mirrors = mirrors
@@ -1136,12 +1246,14 @@ class DisplayReportPage(Page):
default_complete = True
def create_widget(self):
+ _assert_context(ui_context)
self.view = Gtk.TextView()
self.view.set_editable(False)
scrolled = create_scrollable(self.view)
return scrolled
def execute(self, message, *args):
+ _assert_context(ui_context)
# 'use' args only if it's passed
if args:
message = message % args
@@ -1152,6 +1264,7 @@ class LongMessagePage(Page):
default_complete = True
def create_widget(self):
+ _assert_context(ui_context)
self.label = Gtk.Label()
self.label.set_line_wrap(True)
self.label.set_justify(Gtk.Justification.FILL)
@@ -1162,6 +1275,7 @@ class LongMessagePage(Page):
return eb
def execute(self, message, *args):
+ _assert_context(ui_context)
message = message % args
# make it all on one line, it will be wrapped at display-time
message = ' '.join(message.split())
@@ -1177,12 +1291,14 @@ class FinalMessagePage(LongMessagePage):
default_complete = True
def execute(self, *args, **kwargs):
+ _assert_context(ui_context)
LongMessagePage.execute(self, *args, **kwargs)
self.set_page_title("Thanks for your report")
class EditorPage(Page):
def create_widget(self):
+ _assert_context(ui_context)
vbox = Gtk.VBox(spacing=6)
hbox = Gtk.HBox(spacing=12)
hbox.pack_start(Gtk.Label("Subject: "), False, True, 0)
@@ -1222,16 +1338,19 @@ class EditorPage(Page):
def switch_out(self):
global report_message
+ _assert_context(ui_context)
report_message = self.get_value()[0]
f = open(self.filename, "w")
f.write(report_message)
f.close()
def connect_signals(self):
+ _assert_context(ui_context)
self.info_buffer.connect('changed', self.validate)
self.subject.connect('changed', self.validate)
def get_value(self):
+ _assert_context(ui_context)
info = self.info_buffer.get_text(self.info_buffer.get_start_iter(),
self.info_buffer.get_end_iter())
if not info.strip():
@@ -1245,14 +1364,17 @@ class EditorPage(Page):
return(message, message != self.message)
def handle_first_info(self):
+ _assert_context(ui_context)
self.focus_in_id = self.view.connect('focus-in-event', self.on_view_focus_in_event)
def on_view_focus_in_event(self, view, *args):
+ _assert_context(ui_context)
# Empty the buffer only the first time
self.info_buffer.set_text("")
view.disconnect(self.focus_in_id)
def execute(self, message, filename, editor, charset='utf-8'):
+ _assert_context(ui_context)
self.message = message
self.report = BugReport(message)
self.filename = filename
@@ -1271,6 +1393,7 @@ class SelectOptionsPage(Page):
default_complete = False
def create_widget(self):
+ _assert_context(ui_context)
self.label = Gtk.Label()
self.label.set_line_wrap(True)
self.label.set_justify(Gtk.Justification.FILL)
@@ -1280,14 +1403,17 @@ class SelectOptionsPage(Page):
return self.vbox
def on_clicked(self, button, menuopt):
+ _assert_context(ui_context)
self.application.set_next_value(menuopt)
self.assistant.forward_page()
def on_display_clicked(self, button):
global report_message
+ _assert_context(ui_context)
ReportViewerDialog(report_message)
def setup_focus(self):
+ _assert_context(ui_context)
if self.default:
self.default.props.can_default = True
self.default.props.has_default = True
@@ -1295,6 +1421,7 @@ class SelectOptionsPage(Page):
self.default.grab_focus()
def execute(self, prompt, menuopts, options):
+ _assert_context(ui_context)
# remove text UI indication
prompt = prompt.replace('(e to edit)', '')
GLib.idle_add(self.label.set_text, prompt)
@@ -1336,6 +1463,7 @@ class SystemPage(Page):
default_complete = False
def create_widget(self):
+ _assert_context(ui_context)
hbox = Gtk.HBox()
self.terminal = Vte.Terminal()
@@ -1351,10 +1479,12 @@ class SystemPage(Page):
return hbox
def on_child_exited(self, terminal):
+ _assert_context(ui_context)
self.application.set_next_value(None)
self.assistant.forward_page()
def execute(self, cmdline):
+ _assert_context(ui_context)
self.terminal.fork_command('/bin/bash', ['/bin/bash', '-c', cmdline])
@@ -1362,10 +1492,12 @@ class ProgressPage(Page):
page_type = Gtk.AssistantPageType.PROGRESS
def pulse(self):
+ _assert_context(ui_context)
self.progress.pulse()
return True
def create_widget(self):
+ _assert_context(ui_context)
vbox = Gtk.VBox(spacing=6)
self.label = Gtk.Label()
self.label.set_line_wrap(True)
@@ -1378,14 +1510,17 @@ class ProgressPage(Page):
return vbox
def set_label(self, text):
+ _assert_context(ui_context)
GLib.idle_add(self.label.set_text, text)
def reset_label(self):
+ _assert_context(ui_context)
self.set_label("This operation may take a while")
class ReportbugAssistant(Gtk.Assistant):
def __init__(self, application):
+ _assert_context(ui_context)
Gtk.Assistant.__init__(self)
self.application = application
@@ -1400,6 +1535,7 @@ class ReportbugAssistant(Gtk.Assistant):
self.setup_pages()
def _hack_buttons(self, widget):
+ _assert_context(ui_context)
# This is a real hack for two reasons:
# 1. There's no other way to access action area but inspecting the assistant and searching for the back button
# 2. Hide back button on show, because it can be shown-hidden by the assistant depending on the page
@@ -1426,18 +1562,22 @@ class ReportbugAssistant(Gtk.Assistant):
widget.forall(self._hack_buttons)
def hack_buttons(self):
+ _assert_context(ui_context)
self._hack_buttons(self)
def connect_signals(self):
+ _assert_context(ui_context)
self.connect('cancel', self.confirm_exit)
self.connect('prepare', self.on_prepare)
self.connect('delete-event', self.close)
self.connect('apply', self.close)
def on_back_show(self, widget):
+ _assert_context(ui_context)
widget.hide()
def on_prepare(self, assistant, widget):
+ _assert_context(ui_context)
# If the user goes back then forward, we must ensure the feedback value to reportbug must be sent
# when the user clicks on "Forward" to the requested page by reportbug
if self.showing_page and self.showing_page == self.requested_page and self.get_current_page() > self.showing_page.page_num:
@@ -1454,9 +1594,11 @@ class ReportbugAssistant(Gtk.Assistant):
GLib.idle_add(self.showing_page.setup_focus)
def close(self, *args):
+ _assert_context(ui_context)
sys.exit(0)
def confirm_exit(self, *args):
+ _assert_context(ui_context)
dialog = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.WARNING, Gtk.ButtonsType.YES_NO,
"Are you sure you want to quit Reportbug?")
@@ -1466,22 +1608,26 @@ class ReportbugAssistant(Gtk.Assistant):
sys.exit(0)
def forward(self, page_num):
+ _assert_context(ui_context)
return page_num + 1
def forward_page(self):
+ _assert_context(ui_context)
self.set_current_page(self.forward(self.showing_page.page_num))
def set_next_page(self, page):
+ _assert_context(ui_context)
self.requested_page = page
# If we're in progress immediately show this guy
if self.showing_page == self.progress_page:
self.set_current_page(page.page_num)
- # Called in UI thread
def set_progress_label(self, text, *args, **kwargs):
+ _assert_context(ui_context)
self.progress_page.set_label(text % args)
def setup_pages(self):
+ _assert_context(ui_context)
# We insert pages between the intro and the progress, so that we give the user the feedback
# that the applications is still running when he presses the "Forward" button
self.showing_page = IntroPage(self)
@@ -1494,17 +1640,20 @@ class ReportbugAssistant(Gtk.Assistant):
# Dialogs
class YesNoDialog(ReportbugConnector, Gtk.MessageDialog):
def __init__(self, application):
+ _assert_context(ui_context)
Gtk.MessageDialog.__init__(self, assistant, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO)
self.application = application
self.connect('response', self.on_response)
def on_response(self, dialog, res):
+ _assert_context(ui_context)
self.application.set_next_value(res == Gtk.ResponseType.YES)
self.application.put_next_value()
self.destroy()
def execute_operation(self, msg, yeshelp=None, nohelp=None, default=True, nowrap=False):
+ _assert_context(ui_context)
self.set_markup(msg)
if default:
self.set_default_response(Gtk.ResponseType.YES)
@@ -1515,28 +1664,33 @@ class YesNoDialog(ReportbugConnector, Gtk.MessageDialog):
class DisplayFailureDialog(ReportbugConnector, Gtk.MessageDialog):
def __init__(self, application):
+ _assert_context(ui_context)
Gtk.MessageDialog.__init__(self, assistant, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.WARNING, Gtk.ButtonsType.CLOSE)
self.application = application
self.connect('response', self.on_response)
def on_response(self, dialog, res):
+ _assert_context(ui_context)
self.application.put_next_value()
self.destroy()
def execute_operation(self, msg, *args):
+ _assert_context(ui_context)
self.set_markup(msg % args)
self.show_all()
class GetFilenameDialog(ReportbugConnector, Gtk.FileChooserDialog):
def __init__(self, application):
+ _assert_context(ui_context)
Gtk.FileChooserDialog.__init__(self, '', assistant, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
self.application = application
self.connect('response', self.on_response)
def on_response(self, dialog, res):
+ _assert_context(ui_context)
value = None
if res == Gtk.ResponseType.OK:
value = self.get_filename()
@@ -1546,22 +1700,25 @@ class GetFilenameDialog(ReportbugConnector, Gtk.FileChooserDialog):
self.destroy()
def execute_operation(self, title, force_prompt=False):
+ _assert_context(ui_context)
self.set_title(ask_free(title))
self.show_all()
-# Called in reportbug thread
def log_message(*args, **kwargs):
+ _assert_context(reportbug_context)
application.run_once_in_main_thread(assistant.set_progress_label, *args, **kwargs)
def select_multiple(*args, **kwargs):
+ _assert_context(reportbug_context)
kwargs['multiple'] = True
kwargs['empty_ok'] = True
return menu(*args, **kwargs)
def get_multiline(prompt, *args, **kwargs):
+ _assert_context(reportbug_context)
if 'ENTER' in prompt:
# This is a list, let's handle it the best way
return get_list(prompt, *args, **kwargs)
@@ -1588,7 +1745,9 @@ dialogs = {'yes_no': YesNoDialog,
def create_forwarder(parent, klass):
+ _assert_context(reportbug_context)
def func(*args, **kwargs):
+ _assert_context(reportbug_context)
op = application.call_in_main_thread(klass, parent)
try:
args, kwargs = op.sync_pre_operation(*args, **kwargs)
@@ -1600,6 +1759,7 @@ def create_forwarder(parent, klass):
def forward_operations(parent, operations):
+ _assert_context(reportbug_context)
for operation, klass in operations.items():
globals()[operation] = create_forwarder(parent, klass)
@@ -1654,4 +1814,5 @@ Falling back to 'text' interface."""
def can_input():
+ _assert_context(reportbug_context)
return True
--
2.11.0
_______________________________________________
Reportbug-maint mailing list
[email protected]
http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/reportbug-maint