commit 3d16afe6bb18a78a200eb206f14720c329b90626 Author: Sean Davis Date: Fri Jul 12 13:56:24 2013 -0400 Initial project creation with Quickly! diff --git a/.quickly b/.quickly new file mode 100644 index 0000000..338c839 --- /dev/null +++ b/.quickly @@ -0,0 +1,3 @@ +project = mugshot +version = 12.08.1 +template = ubuntu-application diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..7b062b0 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Copyright (C) YYYY diff --git a/bin/mugshot b/bin/mugshot new file mode 100755 index 0000000..e5646f6 --- /dev/null +++ b/bin/mugshot @@ -0,0 +1,36 @@ +#!/usr/bin/python +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +### DO NOT EDIT THIS FILE ### + +import sys +import os + +import locale +locale.textdomain('mugshot') + +# Add project root directory (enable symlink and trunk execution) +PROJECT_ROOT_DIRECTORY = os.path.abspath( + os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0])))) + +python_path = [] +if os.path.abspath(__file__).startswith('/opt'): + locale.bindtextdomain('mugshot', '/opt/extras.ubuntu.com/mugshot/share/locale') + syspath = sys.path[:] # copy to avoid infinite loop in pending objects + for path in syspath: + opt_path = path.replace('/usr', '/opt/extras.ubuntu.com/mugshot') + python_path.insert(0, opt_path) + sys.path.insert(0, opt_path) + os.putenv("XDG_DATA_DIRS", "%s:%s" % ("/opt/extras.ubuntu.com/mugshot/share/", os.getenv("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/"))) +if (os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'mugshot')) + and PROJECT_ROOT_DIRECTORY not in sys.path): + python_path.insert(0, PROJECT_ROOT_DIRECTORY) + sys.path.insert(0, PROJECT_ROOT_DIRECTORY) +if python_path: + os.putenv('PYTHONPATH', "%s:%s" % (os.getenv('PYTHONPATH', ''), ':'.join(python_path))) # for subprocesses + +import mugshot +mugshot.main() diff --git a/data/glib-2.0/schemas/net.launchpad.mugshot.gschema.xml b/data/glib-2.0/schemas/net.launchpad.mugshot.gschema.xml new file mode 100644 index 0000000..285bcb0 --- /dev/null +++ b/data/glib-2.0/schemas/net.launchpad.mugshot.gschema.xml @@ -0,0 +1,10 @@ + + + + + '' + Sample setting + Longer description of this sample setting. Talk about allowed values and what it does. + + + diff --git a/data/media/background.png b/data/media/background.png new file mode 100644 index 0000000..7bd54d7 Binary files /dev/null and b/data/media/background.png differ diff --git a/data/media/mugshot.svg b/data/media/mugshot.svg new file mode 100644 index 0000000..5f39371 --- /dev/null +++ b/data/media/mugshot.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/data/ui/AboutMugshotDialog.ui b/data/ui/AboutMugshotDialog.ui new file mode 100644 index 0000000..d168d5d --- /dev/null +++ b/data/ui/AboutMugshotDialog.ui @@ -0,0 +1,38 @@ + + + + + + False + 5 + ../media/mugshot.svg + normal + Mugshot + + ../media/mugshot.svg + + + True + False + vertical + 2 + + + True + False + end + + + False + True + end + 0 + + + + + + + + + diff --git a/data/ui/MugshotWindow.ui b/data/ui/MugshotWindow.ui new file mode 100644 index 0000000..ba8894a --- /dev/null +++ b/data/ui/MugshotWindow.ui @@ -0,0 +1,298 @@ + + + + + + + True + False + gtk-help + + + False + Mugshot + ../media/mugshot.svg + + + True + False + 5 + + + True + False + + + True + False + False + _File + True + + + True + False + + + gtk-new + True + False + False + True + True + + + + + + gtk-open + True + False + False + True + True + + + + + + True + False + + + + + gtk-save + True + False + False + True + True + + + + + + gtk-save-as + True + False + False + True + True + + + + + + True + False + + + + + gtk-close + True + False + False + True + True + + + + + + + + + + True + False + False + _Edit + True + + + True + False + + + gtk-cut + True + False + False + True + True + + + + + + gtk-copy + True + False + False + True + True + + + + + + gtk-paste + True + False + False + True + True + + + + + + gtk-delete + True + False + False + True + True + + + + + + True + False + + + + + gtk-preferences + True + False + False + True + True + + + + + + + + + True + False + False + _View + True + + + + + True + False + False + _Help + True + + + True + False + + + Contents + True + False + False + image2 + False + + + + + + gtk-about + True + False + False + True + True + + + + + + + + + False + True + 0 + + + + + True + False + 30 + 5 + Your application has been created! + +To start changing this user interface, run 'quickly design', which will open Glade so you can edit the default windows and dialogs. + +To change the behavior and edit the python code, run 'quickly edit', which will bring up a text editor. + True + + + True + True + 1 + + + + + True + False + 5 + 5 + ../media/background.png + + + True + True + 15 + 2 + + + + + True + False + 2 + + + True + False + 0 + 5 + 5 + Status Area + + + True + True + 0 + + + + + False + True + end + 3 + + + + + + diff --git a/data/ui/PreferencesMugshotDialog.ui b/data/ui/PreferencesMugshotDialog.ui new file mode 100644 index 0000000..aedc25c --- /dev/null +++ b/data/ui/PreferencesMugshotDialog.ui @@ -0,0 +1,112 @@ + + + + + + False + Mugshot Preferences + ../media/mugshot.svg + normal + + + True + False + vertical + 12 + + + True + False + end + + + gtk-help + True + True + True + False + True + + + False + False + 0 + True + + + + + gtk-close + True + True + True + False + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 6 + 6 + 1 + 2 + + + True + True + True + + + + 1 + 0 + 1 + 1 + + + + + True + False + 0 + _Example entry: + True + example_entry + + + 0 + 0 + 1 + 1 + + + + + False + True + 1 + + + + + + btn_help + btn_close + + + diff --git a/data/ui/about_mugshot_dialog.xml b/data/ui/about_mugshot_dialog.xml new file mode 100644 index 0000000..fd40754 --- /dev/null +++ b/data/ui/about_mugshot_dialog.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/data/ui/mugshot_window.xml b/data/ui/mugshot_window.xml new file mode 100644 index 0000000..c11d3b3 --- /dev/null +++ b/data/ui/mugshot_window.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/data/ui/preferences_mugshot_dialog.xml b/data/ui/preferences_mugshot_dialog.xml new file mode 100644 index 0000000..4f04236 --- /dev/null +++ b/data/ui/preferences_mugshot_dialog.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/help/C/figures/icon.png b/help/C/figures/icon.png new file mode 100644 index 0000000..e5cf5ff Binary files /dev/null and b/help/C/figures/icon.png differ diff --git a/help/C/index.page b/help/C/index.page new file mode 100644 index 0000000..e85f0c7 --- /dev/null +++ b/help/C/index.page @@ -0,0 +1,44 @@ + + + + + Mugshot + The Mugshot help. + + Your Name + Your E-mail + 2010 + + +

Creative Commons Attribution-Share Alike 3.0 Unported License

+
+
+ + +<!-- This shows on the page in title font --> +<!-- the icon only shows when installed --> +<media type="image" mime="image/png" src="figures/icon.png">[icon]</media> +<app>Mugshot</app> Help + + +

This is an example guide page. It's main function is to link together the help topics.

+ + +
+ +Contents +
+ + + +

+ Your script or application looks better if it has help files similar to other applications in ubuntu. +

+

+ Some people think that help files are only for apps that are difficult to use. +

+
+ +
diff --git a/help/C/preferences.page b/help/C/preferences.page new file mode 100644 index 0000000..67d6a70 --- /dev/null +++ b/help/C/preferences.page @@ -0,0 +1,18 @@ + + + + + + Your Name + Your E-mail + 2010 + + Optional short description of Preferences for contents page + + +Preferences +

This is the preferences page.

+ +
diff --git a/help/C/topic1.page b/help/C/topic1.page new file mode 100644 index 0000000..1f21e94 --- /dev/null +++ b/help/C/topic1.page @@ -0,0 +1,18 @@ + + + + + + Your Name + Your E-mail + 2010 + + Optional short description of Topic 1 for contents page + + +Topic 1 +

This is an example topic page. It's main function is to describe one topic.

+ +
diff --git a/mugshot.desktop.in b/mugshot.desktop.in new file mode 100644 index 0000000..46fbe95 --- /dev/null +++ b/mugshot.desktop.in @@ -0,0 +1,8 @@ +[Desktop Entry] +_Name=Mugshot +_Comment=Mugshot application +Categories=GNOME;Utility; +Exec=mugshot +Icon=mugshot +Terminal=false +Type=Application diff --git a/mugshot/AboutMugshotDialog.py b/mugshot/AboutMugshotDialog.py new file mode 100644 index 0000000..5653f92 --- /dev/null +++ b/mugshot/AboutMugshotDialog.py @@ -0,0 +1,22 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +from locale import gettext as _ + +import logging +logger = logging.getLogger('mugshot') + +from mugshot_lib.AboutDialog import AboutDialog + +# See mugshot_lib.AboutDialog.py for more details about how this class works. +class AboutMugshotDialog(AboutDialog): + __gtype_name__ = "AboutMugshotDialog" + + def finish_initializing(self, builder): # pylint: disable=E1002 + """Set up the about dialog""" + super(AboutMugshotDialog, self).finish_initializing(builder) + + # Code for other initialization actions should be added here. + diff --git a/mugshot/MugshotWindow.py b/mugshot/MugshotWindow.py new file mode 100644 index 0000000..98a5f3a --- /dev/null +++ b/mugshot/MugshotWindow.py @@ -0,0 +1,28 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +from locale import gettext as _ + +from gi.repository import Gtk # pylint: disable=E0611 +import logging +logger = logging.getLogger('mugshot') + +from mugshot_lib import Window +from mugshot.AboutMugshotDialog import AboutMugshotDialog +from mugshot.PreferencesMugshotDialog import PreferencesMugshotDialog + +# See mugshot_lib.Window.py for more details about how this class works +class MugshotWindow(Window): + __gtype_name__ = "MugshotWindow" + + def finish_initializing(self, builder): # pylint: disable=E1002 + """Set up the main window""" + super(MugshotWindow, self).finish_initializing(builder) + + self.AboutDialog = AboutMugshotDialog + self.PreferencesDialog = PreferencesMugshotDialog + + # Code for other initialization actions should be added here. + diff --git a/mugshot/PreferencesMugshotDialog.py b/mugshot/PreferencesMugshotDialog.py new file mode 100644 index 0000000..93e2559 --- /dev/null +++ b/mugshot/PreferencesMugshotDialog.py @@ -0,0 +1,33 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +# This is your preferences dialog. +# +# Define your preferences in +# data/glib-2.0/schemas/net.launchpad.mugshot.gschema.xml +# See http://developer.gnome.org/gio/stable/GSettings.html for more info. + +from gi.repository import Gio # pylint: disable=E0611 + +from locale import gettext as _ + +import logging +logger = logging.getLogger('mugshot') + +from mugshot_lib.PreferencesDialog import PreferencesDialog + +class PreferencesMugshotDialog(PreferencesDialog): + __gtype_name__ = "PreferencesMugshotDialog" + + def finish_initializing(self, builder): # pylint: disable=E1002 + """Set up the preferences dialog""" + super(PreferencesMugshotDialog, self).finish_initializing(builder) + + # Bind each preference widget to gsettings + settings = Gio.Settings("net.launchpad.mugshot") + widget = self.builder.get_object('example_entry') + settings.bind("example", widget, "text", Gio.SettingsBindFlags.DEFAULT) + + # Code for other initialization actions should be added here. diff --git a/mugshot/__init__.py b/mugshot/__init__.py new file mode 100644 index 0000000..24013e2 --- /dev/null +++ b/mugshot/__init__.py @@ -0,0 +1,33 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +import optparse + +from locale import gettext as _ + +from gi.repository import Gtk # pylint: disable=E0611 + +from mugshot import MugshotWindow + +from mugshot_lib import set_up_logging, get_version + +def parse_options(): + """Support for command line options""" + parser = optparse.OptionParser(version="%%prog %s" % get_version()) + parser.add_option( + "-v", "--verbose", action="count", dest="verbose", + help=_("Show debug messages (-vv debugs mugshot_lib also)")) + (options, args) = parser.parse_args() + + set_up_logging(options) + +def main(): + 'constructor for your class instances' + parse_options() + + # Run the application. + window = MugshotWindow.MugshotWindow() + window.show() + Gtk.main() diff --git a/mugshot_lib/AboutDialog.py b/mugshot_lib/AboutDialog.py new file mode 100644 index 0000000..cdd8646 --- /dev/null +++ b/mugshot_lib/AboutDialog.py @@ -0,0 +1,39 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +### DO NOT EDIT THIS FILE ### + +from gi.repository import Gtk # pylint: disable=E0611 + +from . helpers import get_builder + +class AboutDialog(Gtk.AboutDialog): + __gtype_name__ = "AboutDialog" + + def __new__(cls): + """Special static method that's automatically called by Python when + constructing a new instance of this class. + + Returns a fully instantiated AboutDialog object. + """ + builder = get_builder('AboutMugshotDialog') + new_object = builder.get_object("about_mugshot_dialog") + new_object.finish_initializing(builder) + return new_object + + def finish_initializing(self, builder): + """Called while initializing this instance in __new__ + + finish_initalizing should be called after parsing the ui definition + and creating a AboutDialog object with it in order + to finish initializing the start of the new AboutMugshotDialog + instance. + + Put your initialization code in here and leave __init__ undefined. + """ + # Get a reference to the builder and set up the signals. + self.builder = builder + self.ui = builder.get_ui(self) + diff --git a/mugshot_lib/Builder.py b/mugshot_lib/Builder.py new file mode 100644 index 0000000..87ace17 --- /dev/null +++ b/mugshot_lib/Builder.py @@ -0,0 +1,314 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +### DO NOT EDIT THIS FILE ### + +'''Enhances builder connections, provides object to access glade objects''' + +from gi.repository import GObject, Gtk # pylint: disable=E0611 + +import inspect +import functools +import logging +logger = logging.getLogger('mugshot_lib') + +from xml.etree.cElementTree import ElementTree + +# this module is big so uses some conventional prefixes and postfixes +# *s list, except self.widgets is a dictionary +# *_dict dictionary +# *name string +# ele_* element in a ElementTree + + +# pylint: disable=R0904 +# the many public methods is a feature of Gtk.Builder +class Builder(Gtk.Builder): + ''' extra features + connects glade defined handler to default_handler if necessary + auto connects widget to handler with matching name or alias + auto connects several widgets to a handler via multiple aliases + allow handlers to lookup widget name + logs every connection made, and any on_* not made + ''' + + def __init__(self): + Gtk.Builder.__init__(self) + self.widgets = {} + self.glade_handler_dict = {} + self.connections = [] + self._reverse_widget_dict = {} + +# pylint: disable=R0201 +# this is a method so that a subclass of Builder can redefine it + def default_handler(self, + handler_name, filename, *args, **kwargs): + '''helps the apprentice guru + + glade defined handlers that do not exist come here instead. + An apprentice guru might wonder which signal does what he wants, + now he can define any likely candidates in glade and notice which + ones get triggered when he plays with the project. + this method does not appear in Gtk.Builder''' + logger.debug('''tried to call non-existent function:%s() + expected in %s + args:%s + kwargs:%s''', handler_name, filename, args, kwargs) +# pylint: enable=R0201 + + def get_name(self, widget): + ''' allows a handler to get the name (id) of a widget + + this method does not appear in Gtk.Builder''' + return self._reverse_widget_dict.get(widget) + + def add_from_file(self, filename): + '''parses xml file and stores wanted details''' + Gtk.Builder.add_from_file(self, filename) + + # extract data for the extra interfaces + tree = ElementTree() + tree.parse(filename) + + ele_widgets = tree.getiterator("object") + for ele_widget in ele_widgets: + name = ele_widget.attrib['id'] + widget = self.get_object(name) + + # populate indexes - a dictionary of widgets + self.widgets[name] = widget + + # populate a reversed dictionary + self._reverse_widget_dict[widget] = name + + # populate connections list + ele_signals = ele_widget.findall("signal") + + connections = [ + (name, + ele_signal.attrib['name'], + ele_signal.attrib['handler']) for ele_signal in ele_signals] + + if connections: + self.connections.extend(connections) + + ele_signals = tree.getiterator("signal") + for ele_signal in ele_signals: + self.glade_handler_dict.update( + {ele_signal.attrib["handler"]: None}) + + def connect_signals(self, callback_obj): + '''connect the handlers defined in glade + + reports successful and failed connections + and logs call to missing handlers''' + filename = inspect.getfile(callback_obj.__class__) + callback_handler_dict = dict_from_callback_obj(callback_obj) + connection_dict = {} + connection_dict.update(self.glade_handler_dict) + connection_dict.update(callback_handler_dict) + for item in connection_dict.items(): + if item[1] is None: + # the handler is missing so reroute to default_handler + handler = functools.partial( + self.default_handler, item[0], filename) + + connection_dict[item[0]] = handler + + # replace the run time warning + logger.warn("expected handler '%s' in %s", + item[0], filename) + + # connect glade define handlers + Gtk.Builder.connect_signals(self, connection_dict) + + # let's tell the user how we applied the glade design + for connection in self.connections: + widget_name, signal_name, handler_name = connection + logger.debug("connect builder by design '%s', '%s', '%s'", + widget_name, signal_name, handler_name) + + def get_ui(self, callback_obj=None, by_name=True): + '''Creates the ui object with widgets as attributes + + connects signals by 2 methods + this method does not appear in Gtk.Builder''' + + result = UiFactory(self.widgets) + + # Hook up any signals the user defined in glade + if callback_obj is not None: + # connect glade define handlers + self.connect_signals(callback_obj) + + if by_name: + auto_connect_by_name(callback_obj, self) + + return result + + +# pylint: disable=R0903 +# this class deliberately does not provide any public interfaces +# apart from the glade widgets +class UiFactory(): + ''' provides an object with attributes as glade widgets''' + def __init__(self, widget_dict): + self._widget_dict = widget_dict + for (widget_name, widget) in widget_dict.items(): + setattr(self, widget_name, widget) + + # Mangle any non-usable names (like with spaces or dashes) + # into pythonic ones + cannot_message = """cannot bind ui.%s, name already exists + consider using a pythonic name instead of design name '%s'""" + consider_message = """consider using a pythonic name instead of design name '%s'""" + + for (widget_name, widget) in widget_dict.items(): + pyname = make_pyname(widget_name) + if pyname != widget_name: + if hasattr(self, pyname): + logger.debug(cannot_message, pyname, widget_name) + else: + logger.debug(consider_message, widget_name) + setattr(self, pyname, widget) + + def iterator(): + '''Support 'for o in self' ''' + return iter(widget_dict.values()) + setattr(self, '__iter__', iterator) + + def __getitem__(self, name): + 'access as dictionary where name might be non-pythonic' + return self._widget_dict[name] +# pylint: enable=R0903 + + +def make_pyname(name): + ''' mangles non-pythonic names into pythonic ones''' + pyname = '' + for character in name: + if (character.isalpha() or character == '_' or + (pyname and character.isdigit())): + pyname += character + else: + pyname += '_' + return pyname + + +# Until bug https://bugzilla.gnome.org/show_bug.cgi?id=652127 is fixed, we +# need to reimplement inspect.getmembers. GObject introspection doesn't +# play nice with it. +def getmembers(obj, check): + members = [] + for k in dir(obj): + try: + attr = getattr(obj, k) + except: + continue + if check(attr): + members.append((k, attr)) + members.sort() + return members + + +def dict_from_callback_obj(callback_obj): + '''a dictionary interface to callback_obj''' + methods = getmembers(callback_obj, inspect.ismethod) + + aliased_methods = [x[1] for x in methods if hasattr(x[1], 'aliases')] + + # a method may have several aliases + #~ @alias('on_btn_foo_clicked') + #~ @alias('on_tool_foo_activate') + #~ on_menu_foo_activate(): + #~ pass + alias_groups = [(x.aliases, x) for x in aliased_methods] + + aliases = [] + for item in alias_groups: + for alias in item[0]: + aliases.append((alias, item[1])) + + dict_methods = dict(methods) + dict_aliases = dict(aliases) + + results = {} + results.update(dict_methods) + results.update(dict_aliases) + + return results + + +def auto_connect_by_name(callback_obj, builder): + '''finds handlers like on__ and connects them + + i.e. find widget,signal pair in builder and call + widget.connect(signal, on__)''' + + callback_handler_dict = dict_from_callback_obj(callback_obj) + + for item in builder.widgets.items(): + (widget_name, widget) = item + signal_ids = [] + try: + widget_type = type(widget) + while widget_type: + signal_ids.extend(GObject.signal_list_ids(widget_type)) + widget_type = GObject.type_parent(widget_type) + except RuntimeError: # pylint wants a specific error + pass + signal_names = [GObject.signal_name(sid) for sid in signal_ids] + + # Now, automatically find any the user didn't specify in glade + for sig in signal_names: + # using convention suggested by glade + sig = sig.replace("-", "_") + handler_names = ["on_%s_%s" % (widget_name, sig)] + + # Using the convention that the top level window is not + # specified in the handler name. That is use + # on_destroy() instead of on_windowname_destroy() + if widget is callback_obj: + handler_names.append("on_%s" % sig) + + do_connect(item, sig, handler_names, + callback_handler_dict, builder.connections) + + log_unconnected_functions(callback_handler_dict, builder.connections) + + +def do_connect(item, signal_name, handler_names, + callback_handler_dict, connections): + '''connect this signal to an unused handler''' + widget_name, widget = item + + for handler_name in handler_names: + target = handler_name in callback_handler_dict.keys() + connection = (widget_name, signal_name, handler_name) + duplicate = connection in connections + if target and not duplicate: + widget.connect(signal_name, callback_handler_dict[handler_name]) + connections.append(connection) + + logger.debug("connect builder by name '%s','%s', '%s'", + widget_name, signal_name, handler_name) + + +def log_unconnected_functions(callback_handler_dict, connections): + '''log functions like on_* that we could not connect''' + + connected_functions = [x[2] for x in connections] + + handler_names = callback_handler_dict.keys() + unconnected = [x for x in handler_names if x.startswith('on_')] + + for handler_name in connected_functions: + try: + unconnected.remove(handler_name) + except ValueError: + pass + + for handler_name in unconnected: + logger.debug("Not connected to builder '%s'", handler_name) diff --git a/mugshot_lib/PreferencesDialog.py b/mugshot_lib/PreferencesDialog.py new file mode 100644 index 0000000..9eedb7f --- /dev/null +++ b/mugshot_lib/PreferencesDialog.py @@ -0,0 +1,53 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +### DO NOT EDIT THIS FILE ### + +"""this dialog adjusts values in gsettings +""" + +from gi.repository import Gtk # pylint: disable=E0611 +import logging +logger = logging.getLogger('mugshot_lib') + +from . helpers import get_builder, show_uri, get_help_uri + +class PreferencesDialog(Gtk.Dialog): + __gtype_name__ = "PreferencesDialog" + + def __new__(cls): + """Special static method that's automatically called by Python when + constructing a new instance of this class. + + Returns a fully instantiated PreferencesDialog object. + """ + builder = get_builder('PreferencesMugshotDialog') + new_object = builder.get_object("preferences_mugshot_dialog") + new_object.finish_initializing(builder) + return new_object + + def finish_initializing(self, builder): + """Called while initializing this instance in __new__ + + finish_initalizing should be called after parsing the ui definition + and creating a PreferencesDialog object with it in order to + finish initializing the start of the new PerferencesMugshotDialog + instance. + + Put your initialization code in here and leave __init__ undefined. + """ + + # Get a reference to the builder and set up the signals. + self.builder = builder + self.ui = builder.get_ui(self, True) + + # code for other initialization actions should be added here + + def on_btn_close_clicked(self, widget, data=None): + self.destroy() + + def on_btn_help_clicked(self, widget, data=None): + show_uri(self, "ghelp:%s" % get_help_uri('preferences')) + diff --git a/mugshot_lib/Window.py b/mugshot_lib/Window.py new file mode 100644 index 0000000..46f9bd9 --- /dev/null +++ b/mugshot_lib/Window.py @@ -0,0 +1,118 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +### DO NOT EDIT THIS FILE ### + +from gi.repository import Gio, Gtk # pylint: disable=E0611 +import logging +logger = logging.getLogger('mugshot_lib') + +from . helpers import get_builder, show_uri, get_help_uri + +# This class is meant to be subclassed by MugshotWindow. It provides +# common functions and some boilerplate. +class Window(Gtk.Window): + __gtype_name__ = "Window" + + # To construct a new instance of this method, the following notable + # methods are called in this order: + # __new__(cls) + # __init__(self) + # finish_initializing(self, builder) + # __init__(self) + # + # For this reason, it's recommended you leave __init__ empty and put + # your initialization code in finish_initializing + + def __new__(cls): + """Special static method that's automatically called by Python when + constructing a new instance of this class. + + Returns a fully instantiated BaseMugshotWindow object. + """ + builder = get_builder('MugshotWindow') + new_object = builder.get_object("mugshot_window") + new_object.finish_initializing(builder) + return new_object + + def finish_initializing(self, builder): + """Called while initializing this instance in __new__ + + finish_initializing should be called after parsing the UI definition + and creating a MugshotWindow object with it in order to finish + initializing the start of the new MugshotWindow instance. + """ + # Get a reference to the builder and set up the signals. + self.builder = builder + self.ui = builder.get_ui(self, True) + self.PreferencesDialog = None # class + self.preferences_dialog = None # instance + self.AboutDialog = None # class + + self.settings = Gio.Settings("net.launchpad.mugshot") + self.settings.connect('changed', self.on_preferences_changed) + + # Optional application indicator support + # Run 'quickly add indicator' to get started. + # More information: + # http://owaislone.org/quickly-add-indicator/ + # https://wiki.ubuntu.com/DesktopExperienceTeam/ApplicationIndicators + try: + from mugshot import indicator + # self is passed so methods of this class can be called from indicator.py + # Comment this next line out to disable appindicator + self.indicator = indicator.new_application_indicator(self) + except ImportError: + pass + + def on_mnu_contents_activate(self, widget, data=None): + show_uri(self, "ghelp:%s" % get_help_uri()) + + def on_mnu_about_activate(self, widget, data=None): + """Display the about box for mugshot.""" + if self.AboutDialog is not None: + about = self.AboutDialog() # pylint: disable=E1102 + response = about.run() + about.destroy() + + def on_mnu_preferences_activate(self, widget, data=None): + """Display the preferences window for mugshot.""" + + """ From the PyGTK Reference manual + Say for example the preferences dialog is currently open, + and the user chooses Preferences from the menu a second time; + use the present() method to move the already-open dialog + where the user can see it.""" + if self.preferences_dialog is not None: + logger.debug('show existing preferences_dialog') + self.preferences_dialog.present() + elif self.PreferencesDialog is not None: + logger.debug('create new preferences_dialog') + self.preferences_dialog = self.PreferencesDialog() # pylint: disable=E1102 + self.preferences_dialog.connect('destroy', self.on_preferences_dialog_destroyed) + self.preferences_dialog.show() + # destroy command moved into dialog to allow for a help button + + def on_mnu_close_activate(self, widget, data=None): + """Signal handler for closing the MugshotWindow.""" + self.destroy() + + def on_destroy(self, widget, data=None): + """Called when the MugshotWindow is closed.""" + # Clean up code for saving application state should be added here. + Gtk.main_quit() + + def on_preferences_changed(self, settings, key, data=None): + logger.debug('preference changed: %s = %s' % (key, str(settings.get_value(key)))) + + def on_preferences_dialog_destroyed(self, widget, data=None): + '''only affects gui + + logically there is no difference between the user closing, + minimising or ignoring the preferences dialog''' + logger.debug('on_preferences_dialog_destroyed') + # to determine whether to create or present preferences_dialog + self.preferences_dialog = None + diff --git a/mugshot_lib/__init__.py b/mugshot_lib/__init__.py new file mode 100644 index 0000000..00a6cea --- /dev/null +++ b/mugshot_lib/__init__.py @@ -0,0 +1,14 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +### DO NOT EDIT THIS FILE ### + +'''facade - makes mugshot_lib package easy to refactor + +while keeping its api constant''' +from . helpers import set_up_logging +from . Window import Window +from . mugshotconfig import get_version + diff --git a/mugshot_lib/helpers.py b/mugshot_lib/helpers.py new file mode 100644 index 0000000..34d1259 --- /dev/null +++ b/mugshot_lib/helpers.py @@ -0,0 +1,100 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +### DO NOT EDIT THIS FILE ### + +"""Helpers for an Ubuntu application.""" +import logging +import os + +from . mugshotconfig import get_data_file +from . Builder import Builder + +from locale import gettext as _ + +def get_builder(builder_file_name): + """Return a fully-instantiated Gtk.Builder instance from specified ui + file + + :param builder_file_name: The name of the builder file, without extension. + Assumed to be in the 'ui' directory under the data path. + """ + # Look for the ui file that describes the user interface. + ui_filename = get_data_file('ui', '%s.ui' % (builder_file_name,)) + if not os.path.exists(ui_filename): + ui_filename = None + + builder = Builder() + builder.set_translation_domain('mugshot') + builder.add_from_file(ui_filename) + return builder + + +# Owais Lone : To get quick access to icons and stuff. +def get_media_file(media_file_name): + media_filename = get_data_file('media', '%s' % (media_file_name,)) + if not os.path.exists(media_filename): + media_filename = None + + return "file:///"+media_filename + +class NullHandler(logging.Handler): + def emit(self, record): + pass + +def set_up_logging(opts): + # add a handler to prevent basicConfig + root = logging.getLogger() + null_handler = NullHandler() + root.addHandler(null_handler) + + formatter = logging.Formatter("%(levelname)s:%(name)s: %(funcName)s() '%(message)s'") + + logger = logging.getLogger('mugshot') + logger_sh = logging.StreamHandler() + logger_sh.setFormatter(formatter) + logger.addHandler(logger_sh) + + lib_logger = logging.getLogger('mugshot_lib') + lib_logger_sh = logging.StreamHandler() + lib_logger_sh.setFormatter(formatter) + lib_logger.addHandler(lib_logger_sh) + + # Set the logging level to show debug messages. + if opts.verbose: + logger.setLevel(logging.DEBUG) + logger.debug('logging enabled') + if opts.verbose > 1: + lib_logger.setLevel(logging.DEBUG) + +def get_help_uri(page=None): + # help_uri from source tree - default language + here = os.path.dirname(__file__) + help_uri = os.path.abspath(os.path.join(here, '..', 'help', 'C')) + + if not os.path.exists(help_uri): + # installed so use gnome help tree - user's language + help_uri = 'mugshot' + + # unspecified page is the index.page + if page is not None: + help_uri = '%s#%s' % (help_uri, page) + + return help_uri + +def show_uri(parent, link): + from gi.repository import Gtk # pylint: disable=E0611 + screen = parent.get_screen() + Gtk.show_uri(screen, link, Gtk.get_current_event_time()) + +def alias(alternative_function_name): + '''see http://www.drdobbs.com/web-development/184406073#l9''' + def decorator(function): + '''attach alternative_function_name(s) to function''' + if not hasattr(function, 'aliases'): + function.aliases = [] + function.aliases.append(alternative_function_name) + return function + return decorator diff --git a/mugshot_lib/mugshotconfig.py b/mugshot_lib/mugshotconfig.py new file mode 100644 index 0000000..9f47761 --- /dev/null +++ b/mugshot_lib/mugshotconfig.py @@ -0,0 +1,58 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +### DO NOT EDIT THIS FILE ### + +__all__ = [ + 'project_path_not_found', + 'get_data_file', + 'get_data_path', + ] + +# Where your project will look for your data (for instance, images and ui +# files). By default, this is ../data, relative your trunk layout +__mugshot_data_directory__ = '../data/' +__license__ = '' +__version__ = 'VERSION' + +import os + +from locale import gettext as _ + +class project_path_not_found(Exception): + """Raised when we can't find the project directory.""" + + +def get_data_file(*path_segments): + """Get the full path to a data file. + + Returns the path to a file underneath the data directory (as defined by + `get_data_path`). Equivalent to os.path.join(get_data_path(), + *path_segments). + """ + return os.path.join(get_data_path(), *path_segments) + + +def get_data_path(): + """Retrieve mugshot data path + + This path is by default /../data/ in trunk + and /usr/share/mugshot in an installed version but this path + is specified at installation time. + """ + + # Get pathname absolute or relative. + path = os.path.join( + os.path.dirname(__file__), __mugshot_data_directory__) + + abs_data_path = os.path.abspath(path) + if not os.path.exists(abs_data_path): + raise project_path_not_found + + return abs_data_path + + +def get_version(): + return __version__ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e2f4b22 --- /dev/null +++ b/setup.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +###################### DO NOT TOUCH THIS (HEAD TO THE SECOND PART) ###################### + +import os +import sys + +try: + import DistUtilsExtra.auto +except ImportError: + print >> sys.stderr, 'To build mugshot you need https://launchpad.net/python-distutils-extra' + sys.exit(1) +assert DistUtilsExtra.auto.__version__ >= '2.18', 'needs DistUtilsExtra.auto >= 2.18' + +def update_config(libdir, values = {}): + + filename = os.path.join(libdir, 'mugshot_lib/mugshotconfig.py') + oldvalues = {} + try: + fin = file(filename, 'r') + fout = file(filename + '.new', 'w') + + for line in fin: + fields = line.split(' = ') # Separate variable from value + if fields[0] in values: + oldvalues[fields[0]] = fields[1].strip() + line = "%s = %s\n" % (fields[0], values[fields[0]]) + fout.write(line) + + fout.flush() + fout.close() + fin.close() + os.rename(fout.name, fin.name) + except (OSError, IOError), e: + print ("ERROR: Can't find %s" % filename) + sys.exit(1) + return oldvalues + + +def move_desktop_file(root, target_data, prefix): + # The desktop file is rightly installed into install_data. But it should + # always really be installed into prefix, because while we can install + # normal data files anywhere we want, the desktop file needs to exist in + # the main system to be found. Only actually useful for /opt installs. + + old_desktop_path = os.path.normpath(root + target_data + + '/share/applications') + old_desktop_file = old_desktop_path + '/mugshot.desktop' + desktop_path = os.path.normpath(root + prefix + '/share/applications') + desktop_file = desktop_path + '/mugshot.desktop' + + if not os.path.exists(old_desktop_file): + print ("ERROR: Can't find", old_desktop_file) + sys.exit(1) + elif target_data != prefix + '/': + # This is an /opt install, so rename desktop file to use extras- + desktop_file = desktop_path + '/extras-mugshot.desktop' + try: + os.makedirs(desktop_path) + os.rename(old_desktop_file, desktop_file) + os.rmdir(old_desktop_path) + except OSError as e: + print ("ERROR: Can't rename", old_desktop_file, ":", e) + sys.exit(1) + + return desktop_file + +def update_desktop_file(filename, target_pkgdata, target_scripts): + + try: + fin = file(filename, 'r') + fout = file(filename + '.new', 'w') + + for line in fin: + if 'Icon=' in line: + line = "Icon=%s\n" % (target_pkgdata + 'media/mugshot.svg') + elif 'Exec=' in line: + cmd = line.split("=")[1].split(None, 1) + line = "Exec=%s" % (target_scripts + 'mugshot') + if len(cmd) > 1: + line += " %s" % cmd[1].strip() # Add script arguments back + line += "\n" + fout.write(line) + fout.flush() + fout.close() + fin.close() + os.rename(fout.name, fin.name) + except (OSError, IOError), e: + print ("ERROR: Can't find %s" % filename) + sys.exit(1) + +def compile_schemas(root, target_data): + if target_data == '/usr/': + return # /usr paths don't need this, they will be handled by dpkg + schemadir = os.path.normpath(root + target_data + 'share/glib-2.0/schemas') + if (os.path.isdir(schemadir) and + os.path.isfile('/usr/bin/glib-compile-schemas')): + os.system('/usr/bin/glib-compile-schemas "%s"' % schemadir) + + +class InstallAndUpdateDataDirectory(DistUtilsExtra.auto.install_auto): + def run(self): + DistUtilsExtra.auto.install_auto.run(self) + + target_data = '/' + os.path.relpath(self.install_data, self.root) + '/' + target_pkgdata = target_data + 'share/mugshot/' + target_scripts = '/' + os.path.relpath(self.install_scripts, self.root) + '/' + + values = {'__mugshot_data_directory__': "'%s'" % (target_pkgdata), + '__version__': "'%s'" % self.distribution.get_version()} + update_config(self.install_lib, values) + + desktop_file = move_desktop_file(self.root, target_data, self.prefix) + update_desktop_file(desktop_file, target_pkgdata, target_scripts) + compile_schemas(self.root, target_data) + + +################################################################################## +###################### YOU SHOULD MODIFY ONLY WHAT IS BELOW ###################### +################################################################################## + +DistUtilsExtra.auto.setup( + name='mugshot', + version='0.1', + #license='GPL-3', + #author='Your Name', + #author_email='email@ubuntu.com', + #description='UI for managing …', + #long_description='Here a longer description', + #url='https://launchpad.net/mugshot', + cmdclass={'install': InstallAndUpdateDataDirectory} + ) + diff --git a/tests/test_example.py b/tests/test_example.py new file mode 100644 index 0000000..2560f47 --- /dev/null +++ b/tests/test_example.py @@ -0,0 +1,26 @@ +#!/usr/bin/python +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +import sys +import os.path +import unittest +sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))) + +from mugshot import AboutMugshotDialog + +class TestExample(unittest.TestCase): + def setUp(self): + self.AboutMugshotDialog_members = [ + 'AboutDialog', 'AboutMugshotDialog', 'gettext', 'logger', 'logging'] + + def test_AboutMugshotDialog_members(self): + all_members = dir(AboutMugshotDialog) + public_members = [x for x in all_members if not x.startswith('_')] + public_members.sort() + self.assertEqual(self.AboutMugshotDialog_members, public_members) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_lint.py b/tests/test_lint.py new file mode 100644 index 0000000..5d59a3e --- /dev/null +++ b/tests/test_lint.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# This file is in the public domain +### END LICENSE + +import unittest +import subprocess + +class TestPylint(unittest.TestCase): + def test_project_errors_only(self): + '''run pylint in error only mode + + your code may well work even with pylint errors + but have some unusual code''' + return_code = subprocess.call(["pylint", '-E', 'mugshot']) + # not needed because nosetests displays pylint console output + #self.assertEqual(return_code, 0) + + # un-comment the following for loads of diagnostics + #~ def test_project_full_report(self): + #~ '''Only for the brave +#~ + #~ you will have to make judgement calls about your code standards + #~ that differ from the norm''' + #~ return_code = subprocess.call(["pylint", 'mugshot']) + +if __name__ == '__main__': + 'you will get better results with nosetests' + unittest.main()