Introduce perspectives to manage different screens

The welcome and error screens both require a different "perspective"
into Piper, while being in the same window. For this reason, we
introduce a titlebar and a regular stack and the concept of a
"perspective", which is defined as a certain view into Piper.

A perspective needs to implement an interface of sorts of two methods:
one to retrieve its string name, and another to retrieve its widget titlebar.

Different scenarios can then show different perspectives, that have full
control over the main widget and the titlebar displayed. This commit
introduces the MousePerspective, which is a perspective showing the
mouse configuration. Future commits will add an ErrorPerspective and a
WelcomePerspective, and even further into the future we can add a
KeyboardPerspective as well.
This commit is contained in:
Jente Hidskes
2017-08-02 12:53:30 +02:00
committed by Peter Hutterer
parent d452c50e6b
commit 52bb5e3781
5 changed files with 380 additions and 299 deletions

View File

@@ -7,9 +7,10 @@
<file preprocess="xml-stripblanks">AboutDialog.ui</file>
<file preprocess="xml-stripblanks">ui/ButtonDialog.ui</file>
<file preprocess="xml-stripblanks">ui/ButtonRow.ui</file>
<file preprocess="xml-stripblanks">ui/MousePerspective.ui</file>
<file preprocess="xml-stripblanks">ui/LedDialog.ui</file>
<file preprocess="xml-stripblanks">ui/OptionButton.ui</file>
<file preprocess="xml-stripblanks">ui/ProfileRow.ui</file>
<file preprocess="xml-stripblanks">ui/ProfileRow.ui</file>
<file preprocess="xml-stripblanks">ui/ResolutionRow.ui</file>
<file preprocess="xml-stripblanks">ui/ResolutionsPage.ui</file>
<file preprocess="xml-stripblanks">ui/Window.ui</file>

202
data/ui/MousePerspective.ui Normal file
View File

@@ -0,0 +1,202 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="MousePerspective" parent="GtkOverlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_duration">400</property>
<property name="transition_type">slide-left-right</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkRevealer" id="notification_commit">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">start</property>
<property name="transition_duration">100</property>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="notification_commit_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">30</property>
<property name="margin_end">30</property>
<property name="label" translatable="yes">Failed to commit changes to the device</property>
<property name="ellipsize">middle</property>
<property name="max_width_chars">50</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="notification_commit_close">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<signal name="clicked" handler="_on_notification_commit_close_clicked" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">window-close-symbolic</property>
<property name="icon_size">2</property>
</object>
</child>
<style>
<class name="image-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
<style>
<class name="app-notification"/>
</style>
</object>
</child>
</object>
</child>
</template>
<object class="GtkHeaderBar" id="_titlebar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Select another profile</property>
<property name="popover">popover_profiles</property>
<child>
<object class="GtkLabel" id="label_profile">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Profile 1</property>
<property name="justify">center</property>
<property name="track_visited_links">False</property>
</object>
</child>
</object>
</child>
<child type="title">
<object class="GtkStackSwitcher">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stack">stack</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Commit the changes to the device</property>
<signal name="clicked" handler="_on_save_button_clicked" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="titlebar"/>
</style>
</object>
<object class="GtkPopover" id="popover_profiles">
<property name="can_focus">False</property>
<property name="border_width">2</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="propagate_natural_width">True</property>
<property name="propagate_natural_height">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="listbox_profiles">
<property name="width_request">180</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Click to switch to another profile</property>
<signal name="row-activated" handler="_on_profile_row_activated" swapped="no"/>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_profile_button">
<property name="label" translatable="yes">Add profile</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Add a new profile to the device</property>
<signal name="clicked" handler="_on_add_profile_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -2,203 +2,27 @@
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkPopover" id="popover_profiles">
<property name="can_focus">False</property>
<property name="border_width">2</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="propagate_natural_width">True</property>
<property name="propagate_natural_height">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="listbox_profiles">
<property name="width_request">180</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Click to switch to another profile</property>
<signal name="row-activated" handler="_on_profile_row_activated" swapped="no"/>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_profile_button">
<property name="label" translatable="yes">Add profile</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Add a new profile to the device</property>
<signal name="clicked" handler="_on_add_profile_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<template class="Window" parent="GtkApplicationWindow">
<property name="can_focus">False</property>
<child>
<object class="GtkOverlay">
<object class="GtkStack" id="stack_perspectives">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_duration">300</property>
<property name="transition_type">crossfade</property>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_duration">400</property>
<property name="transition_type">slide-left-right</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkRevealer" id="notification_commit">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">start</property>
<property name="transition_duration">100</property>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="notification_commit_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">30</property>
<property name="margin_end">30</property>
<property name="label" translatable="yes">Failed to commit changes to the device</property>
<property name="ellipsize">middle</property>
<property name="max_width_chars">50</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="notification_commit_close">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<signal name="clicked" handler="_on_notification_commit_close_clicked" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">window-close-symbolic</property>
<property name="icon_size">2</property>
</object>
</child>
<style>
<class name="image-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
<style>
<class name="app-notification"/>
</style>
</object>
</child>
</object>
<placeholder/>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar">
<object class="GtkStack" id="stack_titlebar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<property name="hhomogeneous">False</property>
<property name="vhomogeneous">False</property>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Select another profile</property>
<property name="popover">popover_profiles</property>
<child>
<object class="GtkLabel" id="label_profile">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Profile 1</property>
<property name="justify">center</property>
<property name="track_visited_links">False</property>
</object>
</child>
</object>
</child>
<child type="title">
<object class="GtkStackSwitcher">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stack">stack</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Commit the changes to the device</property>
<signal name="clicked" handler="_on_save_button_clicked" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
<placeholder/>
</child>
</object>
</child>

133
piper/mouseperspective.py Normal file
View File

@@ -0,0 +1,133 @@
# Copyright (C) 2017 Jente Hidskes <hjdskes@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 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 General Public License for more details.
#
# You should have received a copy of the GNU 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.
from gettext import gettext as _
from .buttonspage import ButtonsPage
from .gi_composites import GtkTemplate
from .profilerow import ProfileRow
from .ratbagd import RatbagErrorCode, RatbagdDevice
from .resolutionspage import ResolutionsPage
from .ledspage import LedsPage
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, GObject, Gtk
@GtkTemplate(ui="/org/freedesktop/Piper/ui/MousePerspective.ui")
class MousePerspective(Gtk.Overlay):
"""The perspective to configure a mouse."""
__gtype_name__ = "MousePerspective"
_titlebar = GtkTemplate.Child()
stack = GtkTemplate.Child()
notification_commit = GtkTemplate.Child()
listbox_profiles = GtkTemplate.Child()
label_profile = GtkTemplate.Child()
add_profile_button = GtkTemplate.Child()
def __init__(self, *args, **kwargs):
"""Instantiates a new MousePerspective."""
Gtk.Overlay.__init__(self, *args, **kwargs)
self.init_template()
self._device = None
self._notification_commit_timeout_id = 0
@GObject.Property
def name(self):
"""The name of this perspective."""
return "mouse_perspective"
@GObject.Property
def titlebar(self):
"""The titlebar to this perspective."""
return self._titlebar
def set_device(self, device):
self._device = device
capabilities = device.capabilities
self.stack.foreach(Gtk.Widget.destroy)
if RatbagdDevice.CAP_RESOLUTION in capabilities:
self.stack.add_titled(ResolutionsPage(device), "resolutions", _("Resolutions"))
if RatbagdDevice.CAP_BUTTON in capabilities:
self.stack.add_titled(ButtonsPage(device), "buttons", _("Buttons"))
if RatbagdDevice.CAP_LED in capabilities:
self.stack.add_titled(LedsPage(device), "leds", _("LEDs"))
active_profile = device.active_profile
self.label_profile.set_label(_("Profile {}").format(active_profile.index + 1))
# Find the first profile that is enabled. If there is none, disable the
# add button.
left = next((p for p in device.profiles if not p.enabled), None)
self.add_profile_button.set_sensitive(left is not None)
for profile in device.profiles:
profile.connect("notify::enabled", self._on_profile_notify_enabled)
row = ProfileRow(profile)
self.listbox_profiles.insert(row, profile.index)
if profile == active_profile:
self.listbox_profiles.select_row(row)
def _hide_notification_commit(self):
if self._notification_commit_timeout_id is not 0:
GLib.Source.remove(self._notification_commit_timeout_id)
self._notification_commit_timeout_id = 0
self.notification_commit.set_reveal_child(False)
def _show_notification_commit(self):
self.notification_commit.set_reveal_child(True)
self._notification_commit_timeout_id = GLib.timeout_add_seconds(5,
self._on_notification_commit_timeout)
def _on_notification_commit_timeout(self):
self._hide_notification_commit()
return False
@GtkTemplate.Callback
def _on_save_button_clicked(self, button):
status = self._device.commit()
if not status == RatbagErrorCode.RATBAG_SUCCESS:
self._show_notification_commit()
@GtkTemplate.Callback
def _on_notification_commit_close_clicked(self, button):
self._hide_notification_commit()
@GtkTemplate.Callback
def _on_profile_row_activated(self, listbox, row):
row.set_active()
self.label_profile.set_label(row.name)
@GtkTemplate.Callback
def _on_add_profile_button_clicked(self, button):
# Enable the first disabled profile we find.
for profile in self._device.profiles:
if profile.enabled:
continue
profile.enabled = True
if profile == self._device.profiles[-1]:
self.add_profile_button.set_sensitive(False)
break
def _on_profile_notify_enabled(self, profile, pspec):
# We're only interested in the case where the last profile is disabled,
# so that we can reset the sensitivity of the add button.
if not profile.enabled and profile == self._device.profiles[-1]:
self.add_profile_button.set_sensitive(True)

View File

@@ -16,12 +16,8 @@
from gettext import gettext as _
from .buttonspage import ButtonsPage
from .gi_composites import GtkTemplate
from .profilerow import ProfileRow
from .ratbagd import RatbagErrorCode, RatbagdDevice
from .resolutionspage import ResolutionsPage
from .ledspage import LedsPage
from .mouseperspective import MousePerspective
import gi
gi.require_version("Gtk", "3.0")
@@ -31,17 +27,13 @@ from gi.repository import GLib, Gtk
@GtkTemplate(ui="/org/freedesktop/Piper/ui/Window.ui")
class Window(Gtk.ApplicationWindow):
"""A Gtk.ApplicationWindow subclass to implement the main application
window. This Window contains the overlay for the in-app notifications, the
headerbar and the stack holding a ResolutionsPage, a ButtonsPage and a
LedsPage."""
window. This window displays the different perspectives (error, mouse and
welcome) that each present their own behavior."""
__gtype_name__ = "Window"
stack = GtkTemplate.Child()
notification_commit = GtkTemplate.Child()
listbox_profiles = GtkTemplate.Child()
label_profile = GtkTemplate.Child()
add_profile_button = GtkTemplate.Child()
stack_titlebar = GtkTemplate.Child()
stack_perspectives = GtkTemplate.Child()
def __init__(self, ratbag, *args, **kwargs):
"""Instantiates a new Window.
@@ -51,112 +43,41 @@ class Window(Gtk.ApplicationWindow):
Gtk.ApplicationWindow.__init__(self, *args, **kwargs)
self.init_template()
perspectives = [MousePerspective()]
for perspective in perspectives:
self._add_perspective(perspective)
if ratbag is None:
self._present_error_dialog("Cannot connect to ratbagd")
return
self._present_error_perspective(_("Cannot connect to ratbagd"))
elif len(ratbag.devices) == 0:
self._present_error_perspective(_("Cannot find any devices"))
elif len(ratbag.devices) == 1:
self._present_mouse_perspective(ratbag.devices[0])
else:
self._present_welcome_perspective(ratbag.devices)
self._ratbag = ratbag
self._device = self._fetch_ratbag_device()
if self._device is None:
self._present_error_dialog("No devices found")
return
def _add_perspective(self, perspective):
self.stack_perspectives.add_named(perspective, perspective.name)
self.stack_titlebar.add_named(perspective.titlebar, perspective.name)
self._setup_pages()
self._setup_profiles()
def _present_welcome_perspective(self, devices):
# Present the welcome perspective for the user to select one of their
# devices.
pass
def _setup_pages(self):
def _present_mouse_perspective(self, device):
# Present the mouse configuration perspective for the given device.
try:
capabilities = self._device.capabilities
if RatbagdDevice.CAP_RESOLUTION in capabilities:
self.stack.add_titled(ResolutionsPage(self._device), "resolutions", _("Resolutions"))
if RatbagdDevice.CAP_BUTTON in capabilities:
self.stack.add_titled(ButtonsPage(self._device), "buttons", _("Buttons"))
if RatbagdDevice.CAP_LED in capabilities:
self.stack.add_titled(LedsPage(self._device), "leds", _("LEDs"))
mouse_perspective = self.stack_perspectives.get_child_by_name("mouse_perspective")
mouse_perspective.set_device(device)
self.stack_titlebar.set_visible_child_name(mouse_perspective.name)
self.stack_perspectives.set_visible_child_name(mouse_perspective.name)
except ValueError as e:
self._present_error_dialog(e)
self._present_error_perspective(e)
except GLib.Error as e:
self._present_error_dialog(e.message)
self._present_error_perspective(e.message)
def _setup_profiles(self):
active_profile = self._device.active_profile
self.label_profile.set_label(_("Profile {}").format(active_profile.index + 1))
# Find the first profile that is enabled. If there is none, disable the
# add button.
left = next((p for p in self._device.profiles if not p.enabled), None)
self.add_profile_button.set_sensitive(left is not None)
for profile in self._device.profiles:
profile.connect("notify::enabled", self._on_profile_notify_enabled)
row = ProfileRow(profile)
self.listbox_profiles.insert(row, profile.index)
if profile == active_profile:
self.listbox_profiles.select_row(row)
def _present_error_dialog(self, message):
# Present an error dialog informing the user of any errors.
# TODO: this should be something in the window, not a print
print("Cannot create window: {}".format(message))
def _fetch_ratbag_device(self):
"""Get the first ratbag device available. If there are multiple
devices, an error message is printed and we default to the first
one. Otherwise, an error is shown and we return None.
"""
# TODO: replace with better implementation once we go for the welcome screen.
if len(self._ratbag.devices) == 0:
print("Could not find any devices. Do you have anything vaguely mouse-looking plugged in?")
return None
elif len(self._ratbag.devices) > 1:
print("Ooops, can't deal with more than one device. My bad.")
for d in self._ratbag.devices[1:]:
print("Ignoring device {}".format(d.name))
return self._ratbag.devices[0]
def _hide_notification_commit(self):
if self._notification_commit_timeout_id is not 0:
GLib.Source.remove(self._notification_commit_timeout_id)
self._notification_commit_timeout_id = 0
self.notification_commit.set_reveal_child(False)
def _show_notification_commit(self):
self.notification_commit.set_reveal_child(True)
self._notification_commit_timeout_id = GLib.timeout_add_seconds(5,
self._on_notification_commit_timeout)
def _on_notification_commit_timeout(self):
self._hide_notification_commit()
return False
@GtkTemplate.Callback
def _on_save_button_clicked(self, button):
status = self._device.commit()
if not status == RatbagErrorCode.RATBAG_SUCCESS:
self._show_notification_commit()
@GtkTemplate.Callback
def _on_notification_commit_close_clicked(self, button):
self._hide_notification_commit()
@GtkTemplate.Callback
def _on_profile_row_activated(self, listbox, row):
row.set_active()
self.label_profile.set_label(row.name)
@GtkTemplate.Callback
def _on_add_profile_button_clicked(self, button):
# Enable the first disabled profile we find.
for profile in self._device.profiles:
if profile.enabled:
continue
profile.enabled = True
if profile == self._device.profiles[-1]:
self.add_profile_button.set_sensitive(False)
break
def _on_profile_notify_enabled(self, profile, pspec):
# We're only interested in the case where the last profile is disabled,
# so that we can reset the sensitivity of the add button.
if not profile.enabled and profile == self._device.profiles[-1]:
self.add_profile_button.set_sensitive(True)
def _present_error_perspective(self, message):
# Present the error perspective informing the user of any errors.
pass