Subversion Repositories basico

Compare Revisions

Ignore whitespace Rev 3 → Rev 4

/trunk/basico/data/icons/task.png
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
/trunk/basico/data/icons/task.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/export-csv.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/export-csv.png
===================================================================
--- basico/data/icons/export-csv.png (nonexistent)
+++ basico/data/icons/export-csv.png (revision 4)
/basico/data/icons/export-csv.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: AUTHORS
===================================================================
--- AUTHORS (nonexistent)
+++ AUTHORS (revision 4)
@@ -0,0 +1,2 @@
+Tomás Vírseda <tomasvirseda@gmail.com>
+
Index: basico/data/icons/export.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/export.png
===================================================================
--- basico/data/icons/export.png (nonexistent)
+++ basico/data/icons/export.png (revision 4)
/basico/data/icons/export.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: INSTALL
===================================================================
--- INSTALL (nonexistent)
+++ INSTALL (revision 4)
@@ -0,0 +1,4 @@
+Please, follow the instructions from this page:
+
+http://blog.t00mlabs.net/projects/basico/basico-installation
+
Index: basico/data/icons/components.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/components.png
===================================================================
--- basico/data/icons/components.png (nonexistent)
+++ basico/data/icons/components.png (revision 4)
/basico/data/icons/components.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: setup.py
===================================================================
--- setup.py (nonexistent)
+++ setup.py (revision 4)
@@ -0,0 +1,134 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# Authors: Tomás Vírseda <tomasvirseda@gmail.com>
+# Basico is a SAP Notes manager for SAP Consultants
+# Copyright (C) 2016 Tomás Vírseda
+#
+# Basico 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 3 of the License, or
+# (at your option) any later version.
+#
+# Basico 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 Basico. If not, see <http://www.gnu.org/licenses/gpl.html>
+
+import os
+import subprocess
+from setuptools import setup
+
+
+with open('README.rst') as f:
+ long_description = f.read()
+
+def add_data():
+ try:
+ data_files = [
+ ('basico/share/applications', ['basico/data/desktop/basico.desktop']),
+ ('basico/data/icons',
+ [
+ 'basico/data/icons/annotation.png',
+ 'basico/data/icons/bookmark.png',
+ 'basico/data/icons/browse.png',
+ 'basico/data/icons/category.png',
+ 'basico/data/icons/component.png',
+ 'basico/data/icons/delete.png',
+ 'basico/data/icons/noproject.png',
+ 'basico/data/icons/project.png',
+ 'basico/data/icons/sapnote.png',
+ 'basico/data/icons/notask.png',
+ 'basico/data/icons/task.png',
+ 'basico/data/icons/tasks.png',
+ ]),
+ ('basico/data/ui', ['basico/data/ui/basico.ui']),
+ ('basico/data/share', []),
+ ("basico/data/share/docs",
+ [
+ 'LICENSE',
+ 'README.rst',
+ 'INSTALL',
+ 'CREDITS'
+ ]),
+ ]
+
+ if not os.path.isdir('mo'):
+ os.mkdir('mo')
+ for pofile in os.listdir('po'):
+ if pofile.endswith('po'):
+ lang = pofile.strip('.po')
+ modir = os.path.join('mo', lang)
+ if not os.path.isdir(modir):
+ os.mkdir(modir)
+ mofile = os.path.join(modir, 'basico.mo')
+ subprocess.call('msgfmt {} -o {}'.format(os.path.join('po', pofile), mofile), shell=True)
+ data_files.append(['share/locale/{}/LC_MESSAGES/'.format(lang), [mofile]])
+ return data_files
+ except:
+ return []
+
+if os.name == 'posix':
+ data_files = add_data()
+else:
+ data_files = []
+
+try:
+ bcommit = subprocess.check_output("svn info", shell=True)
+ ucommit = bcommit.decode(encoding='UTF-8')
+ icommit = int(ucommit.split('\n')[6].split(':')[1])
+ dcommit = ucommit.split('\n')[11][19:29]
+except Exception as error:
+ print (error)
+ dcommit = 'None'
+ icommit = 0
+
+setup(
+ name='basico',
+ version='0.1',
+ author='Tomás Vírseda',
+ author_email='tomasvirseda@gmail.com',
+ url='https://github.com/t00m/basico',
+ description='SAP Notes manager for SAP Consultants',
+ long_description=long_description,
+ license='GPLv3',
+ packages=['basico'],
+ # distutils does not support install_requires, but pip needs it to be
+ # able to automatically install dependencies
+ install_requires=[
+ 'pygal',
+ 'python-dateutil',
+ 'selenium',
+ 'feedparser',
+ ],
+ include_package_data=True,
+ data_files=data_files,
+ zip_safe=False,
+ platforms='any',
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Environment :: X11 Applications :: Gnome',
+ 'Environment :: X11 Applications :: GTK',
+ 'Intended Audience :: Information Technology',
+ 'Intended Audience :: Other Audience',
+ 'Intended Audience :: System Administrators',
+ 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
+ 'Natural Language :: English',
+ 'Operating System :: Microsoft :: Windows',
+ 'Operating System :: POSIX :: Linux',
+ 'Programming Language :: Python :: 3',
+ 'Topic :: Database :: Front-Ends',
+ 'Topic :: System :: Systems Administration',
+ 'Topic :: Utilities'
+ ],
+ entry_points={
+ #~ 'console_scripts': [
+ #~ 'basico = basico:main',
+ #~ ],
+ 'gui_scripts': [
+ 'basico = basico.basico:main',
+ ]
+ },
+)
Index: basico/iconmanager.py
===================================================================
--- basico/iconmanager.py (nonexistent)
+++ basico/iconmanager.py (revision 4)
@@ -0,0 +1,84 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: menus.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Icon manager service
+
+import os
+
+import pkg_resources
+
+from gi.repository import Gtk
+from gi.repository import Gio
+from gi.repository import Pango
+from gi.repository.GdkPixbuf import Pixbuf
+
+from .service import Service
+
+
+def resource_filename(file_name):
+ paths = map(
+ lambda path: os.path.join(path, file_name),
+ (
+ '/opt/extras.ubuntu.com/',
+ '/usr/local/',
+ '/usr/',
+ ),
+ )
+ for path in paths:
+ if os.path.isfile(path):
+ return path
+ return pkg_resources.resource_filename(
+ pkg_resources.Requirement.parse("basico"), file_name)
+
+class IconManager(Service):
+ def initialize(self):
+ APP_DIR_ICONS = self.app.get_var('ICONS')
+ self.icondict = {}
+ self.theme = Gtk.IconTheme.get_default()
+ self.theme.prepend_search_path (APP_DIR_ICONS)
+
+ def get_themed_icon(self, icon_name):
+ APP_DIR_ICONS = self.app.get_var('ICONS')
+ ICON = APP_DIR_ICONS + icon_name + '.png'
+ #~ self.log.debug(resource_filename(ICON))
+ icon = Gio.ThemedIcon.new(ICON)
+ #~ self.log.debug(type(icon))
+ #~ self.log.debug(dir(icon))
+
+ return icon
+
+
+
+ def get_icon(self, name, width=24, height=24):
+ key = "%s-%d-%d" % (name, width, height)
+
+ # Get icon from cache if exists or add a new one
+ try:
+ icon = self.icondict[key]
+ except:
+ #~ OLD WAY: icon = Pixbuf.new_from_file_at_scale(icon_name, width, height, True)
+ iconinfo = self.theme.lookup_icon(name, width, Gtk.IconLookupFlags.GENERIC_FALLBACK)
+ icon = iconinfo.load_icon()
+ self.icondict[key] = icon
+ return icon
+
+
+ def get_pixbuf_icon(self, name, width=36, height=36):
+ key = "%s-%d-%d" % (name, width, height)
+
+ # Get icon from cache if exists or add a new one
+ try:
+ icon = self.icondict[key]
+ except:
+ #~ OLD WAY: icon = Pixbuf.new_from_file_at_scale(icon_name, width, height, True)
+ iconinfo = self.theme.lookup_icon(name, width, Gtk.IconLookupFlags.GENERIC_FALLBACK)
+ icon = iconinfo.load_icon()
+ self.icondict[key] = icon
+ return icon
+
+
+ def get_image_icon(self, name, width=36, height=36):
+ pixbuf = self.get_pixbuf_icon(name, width, height)
+ return Gtk.Image.new_from_pixbuf(pixbuf)
Index: basico/tasks.py
===================================================================
--- basico/tasks.py (nonexistent)
+++ basico/tasks.py (revision 4)
@@ -0,0 +1,282 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: tasks.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Tasks service
+
+
+from gi.repository import Gtk
+from gi.repository.GdkPixbuf import Pixbuf
+from gi.repository import Pango
+from datetime import datetime
+from dateutil import parser as dateparser
+
+from .service import Service
+
+
+class Tasks(Service):
+ def initialize(self):
+ self.get_services()
+ self.setup_window()
+
+
+ def setup_window(self):
+ # setup widgets
+ self.window = self.gui.add_widget('winTasks')
+ self.window.connect('delete-event', self.hide_window)
+ self.parent = self.gui.get_widget('mainwindow')
+ #~ self.window.set_transient_for(self.parent)
+ self.pname = self.gui.add_widget('etyTaskName')
+ self.pname.connect('activate', self.add_task)
+ self.boxtask = self.gui.add_widget('boxTasks')
+ self.btnadd = self.gui.add_widget('btnAddTask')
+ self.btnadd.connect('clicked', self.add_task)
+ self.btndel = self.gui.add_widget('btnDelTask')
+ self.btndel.connect('clicked', self.delete_task)
+ self.btncancel = self.gui.add_widget('btnCancelTasks')
+ self.btncancel.connect('clicked', self.hide_window)
+ self.btnaccept = self.gui.add_widget('btnAcceptTasks')
+ self.btnaccept.connect('clicked', self.link_to_task)
+ self.treeview = Gtk.TreeView()
+ self.gui.add_widget('trvtaskwin', self.treeview)
+ self.boxtask.add(self.treeview)
+ self.hide_window()
+
+ # setup model
+ model = Gtk.ListStore(
+ bool, # CheckBox
+ str # Task name
+ )
+ self.treeview.set_model(model)
+
+ # setup columns
+ # Checkbox
+ renderer = Gtk.CellRendererToggle()
+ renderer.connect("toggled", self.on_cell_toggled)
+ column = Gtk.TreeViewColumn('X', renderer, active=0)
+ column.set_visible(True)
+ column.set_expand(False)
+ column.set_clickable(False)
+ column.set_sort_indicator(False)
+ self.treeview.append_column(column)
+
+ # Task name
+ renderer = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn('Task name', renderer, text=1)
+ column.set_visible(True)
+ column.set_expand(True)
+ column.set_clickable(True)
+ column.set_sort_indicator(True)
+ self.treeview.append_column(column)
+
+ # Filters
+ #Creating the filter, feeding it with the liststore model
+ #~ self.task_filter = model.filter_new()
+ #setting the filter function, note that we're not using the
+ #~ self.task_filter.set_visible_func(self.task_filter_func)
+ #~ self.task_filter.set_visible_column(1)
+
+ # Treeview features
+ self.treeview.set_headers_visible(True)
+ self.treeview.set_enable_search(True)
+ self.treeview.set_grid_lines(Gtk.TreeViewGridLines.HORIZONTAL)
+ self.treeview.set_search_column(1)
+ self.treeview.connect('row-activated', self.double_click)
+ selection = self.treeview.get_selection()
+ selection.set_mode(Gtk.SelectionMode.SINGLE)
+ self.treeview.set_search_entry(self.pname)
+
+
+ # row task visibility
+ def task_row_visibility(self, model, iter, data):
+ entry = self.gui.get_widget('etyTaskName')
+ text = entry.get_text()
+
+ return text.upper() in model.get_value(iter, 1)
+
+
+ def task_filter_func(self, model, iter, data):
+ """Tests if the task in the row is the one in the filter"""
+ #~ self.log.debug(data)
+ entry = self.gui.get_widget('etyTaskName')
+ task = entry.get_text()
+
+ #~ if len(task) == 0:
+ #~ return False
+
+ task_row = model[iter][1]
+
+ if task.upper() in task_row.upper():
+ return True
+ else:
+ return False
+
+
+ def delete_task(self, *args):
+ found = 0
+ selection = self.treeview.get_selection()
+ result = selection.get_selected()
+ if result: #result could be None
+ model, iter = result
+ task = model.get_value(iter, 1)
+ #~ self.log.debug(task)
+ sapnotes = self.sap.get_notes()
+ for sapnote in sapnotes:
+ try:
+ tasks = sapnotes[sapnote]['tasks']
+ if task in tasks:
+ found += 1
+ except:
+ pass
+ if found > 1:
+ self.log.warning("Task %s is still assigned to other SAP Notes" % task)
+ head = "Task could not be deleted"
+ body = "Task %s is still assigned to other SAP Notes" % task
+ self.uif.message_dialog(head , body)
+ else:
+ model.remove(iter)
+
+
+
+
+ def add_task(self, *args):
+ task = self.pname.get_text()
+ model = self.treeview.get_model()
+
+ found = False
+ for row in model:
+ if row[1].upper() == task.upper():
+ found = True
+
+ if not found and len(task) > 0:
+ model.append([False, task])
+
+ self.pname.set_text('')
+
+
+
+ def show_window(self, sapnotes=[]):
+ rootwin = self.gui.get_widget('mainwindow')
+ #~ self.window.set_transient_for(rootwin)
+ self.load_tasks(sapnotes)
+ self.window.set_no_show_all(False)
+ self.window.show_all()
+ self.window.set_keep_above(True)
+
+
+
+ def hide_window(self, *args):
+ self.window.set_no_show_all(True)
+ self.window.hide()
+
+
+ def get_services(self):
+ self.gui = self.app.get_service("GUI")
+ self.sap = self.app.get_service('SAP')
+ self.uif = self.app.get_service('UIF')
+ self.cb = self.app.get_service('Callbacks')
+
+
+ def double_click(self, treeview, row, col):
+ model = treeview.get_model()
+ self.pname.set_text(model[row][1])
+
+
+ def populate(self, sapnotes):
+ model = self.treeview.get_model()
+ model.clear()
+ #~ model.append([bool, str])
+
+
+ def changed(self, *args):
+ try:
+ model, treeiters = self.selection.get_selected_rows()
+ selected = set()
+ if len(treeiters) > 0:
+ for treeiter in treeiters:
+ if treeiter != None:
+ selected.add(model[treeiter][0])
+ print (selected)
+
+ except Exception as error:
+ self.log.error (self.get_traceback())
+
+
+ def on_cell_toggled(self, widget, path):
+ model = self.treeview.get_model()
+ model[path][0] = not model[path][0]
+
+
+ def get_selected_notes(self):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ return sapnoteview.get_selected_notes()
+
+
+ def link_to_task(self, *args):
+ self.save_tasks()
+ self.hide_window()
+ sapnotes = list(self.get_selected_notes())
+ model = self.treeview.get_model()
+ tasks = []
+ for row in model:
+ link = row[0]
+ if link == True:
+ tasks.append(row[1])
+
+ sapnotes.sort()
+ tasks.sort()
+ #~ self.log.debug('SAP Notes: %s' % sapnotes)
+ #~ self.log.debug(' Tasks: %s' % tasks)
+ self.sap.link_to_task(sapnotes, tasks)
+ self.cb.refresh_view(view='tasks')
+
+
+ def get_all_tasks(self):
+ tasks = []
+ model = self.treeview.get_model()
+ for row in model:
+ tasks.append(row[1])
+ tasks.sort()
+
+ return tasks
+
+
+ def load_tasks(self, sapnotes=[]):
+ model = self.treeview.get_model()
+ model.clear()
+ try:
+ alltasks = set(self.get_config_value('Tasks').split(','))
+ except:
+ alltasks = set()
+
+ #~ self.log.debug("Tasks: %s" % alltasks)
+
+ linkedtasks = set()
+ for sapnote in sapnotes:
+ tasks = self.sap.get_linked_tasks(sapnote)
+ for task in tasks:
+ linkedtasks.add(task)
+
+ for task in alltasks:
+ if task in linkedtasks:
+ model.append([True, task])
+ else:
+ model.append([False, task])
+
+
+
+ def save_tasks(self):
+ settings = {}
+ tasks = self.get_all_tasks()
+
+ settings['Tasks'] = ','.join(tasks)
+ self.config[self.section] = settings
+ self.save_config()
+
+
+ def save_tasks_from_stats(self, alltasks):
+ settings = {}
+ settings['Tasks'] = ','.join(alltasks)
+ self.config[self.section] = settings
+ self.save_config()
Index: basico/stats.py
===================================================================
--- basico/stats.py (nonexistent)
+++ basico/stats.py (revision 4)
@@ -0,0 +1,72 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: stats.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Stats service
+
+import pygal
+from pygal import Config
+
+from .service import Service
+
+
+class Stats(Service):
+ def initialize(self):
+ self.get_services()
+
+ def get_html(self, filename):
+ fin = open(filename, 'r')
+ html = fin.read()
+ fin.close()
+
+ return html
+
+
+ def get_services(self):
+ self.sap = self.app.get_service('SAP')
+
+ def build_pie_maincomp(self):
+ stats = self.sap.get_stats()
+ CHART_FILE = self.app.get_var('TMP', 'local') + 'chart.svg'
+ config = Config()
+ config.show_legend = True
+ config.print_values = True
+ config.print_values_position = 'top'
+ config.print_labels = True
+ #~ config.dynamic_print_values = True
+ config.human_readable = True
+ config.fill = True
+ #~ chart = pygal.XY(config)
+ pie_chart = pygal.HorizontalBar(config)
+ pie_chart.title = 'Main components used'
+ for key in stats['maincomp']:
+ value = stats['maincomp'][key]
+ label = "%s (%d)" % (key, value)
+ pie_chart.add(label, value)
+ pie_chart.render_to_file(CHART_FILE)
+
+ return self.get_html(CHART_FILE)
+
+
+ def build_pie_categories(self):
+ stats = self.sap.get_stats()
+ CHART_FILE = self.app.get_var('TMP', 'local') + 'chart.svg'
+ config = Config()
+ config.show_legend = True
+ config.print_values = True
+ config.print_values_position = 'top'
+ config.print_labels = True
+ #~ config.dynamic_print_values = True
+ config.human_readable = True
+ config.fill = True
+ #~ chart = pygal.XY(config)
+ pie_chart = pygal.HorizontalBar(config)
+ pie_chart.title = 'By categories'
+ for key in stats['cats']:
+ value = stats['cats'][key]
+ label = "%s (%d)" % (key, value)
+ pie_chart.add(label, value)
+ pie_chart.render_to_file(CHART_FILE)
+
+ return self.get_html(CHART_FILE)
Index: Changelog
===================================================================
--- Changelog (nonexistent)
+++ Changelog (revision 4)
@@ -0,0 +1,3 @@
+2016-05-30 Tomás Vírseda <tomasvirseda@gmail.com>
+
+ * Basico 0.1 released
Index: MANIFEST.in
===================================================================
--- MANIFEST.in (nonexistent)
+++ MANIFEST.in (revision 4)
@@ -0,0 +1,2 @@
+recursive-include basico/data *
+recursive-include po *.po
Index: README
===================================================================
--- README (nonexistent)
+++ README (revision 4)
@@ -0,0 +1,39 @@
+basico
+======
+SAP Notes Manager for SAP Consultants
+
+
+Description
+-----------
+The main aim for this application is allow you to download the SAP Notes
+you are interested in, categorize them by tasks in order to have them at
+hand whenever you want and find them quickly.
+
+
+Features
+--------
+- Download SAP Notes
+- Import SAP Notes from other people
+- Export SAP Notes to: plain text, CSV and, JSON format (useful if you
+ want to share your SAP Notes
+- Bookmark those SAP Notes really interesting.
+- Browse SAP Notes in Launchpad
+
+
+Installation
+------------
+Please, follow the instructions from this page:
+http://blog.t00mlabs.net/projects/basico/basico-installation
+
+
+
+Authors
+-------
+Tomás Vírseda <tomasvirseda@gmail.com>
+
+
+Contributors
+------------
+Many thanks to the following people, who have kindly help in this project:
+- Iliya Kuznetsov: ideas to deal with OData protocol and other useful
+ info to start this project
Index: CREDITS
===================================================================
--- CREDITS (nonexistent)
+++ CREDITS (revision 4)
@@ -0,0 +1,2 @@
+Icons (GPL);Web Design Creative;http://findicons.com/pack/2141/web_design_creatives
+Collaborations; your name here...;
Index: basico/uifuncs.py
===================================================================
--- basico/uifuncs.py (nonexistent)
+++ basico/uifuncs.py (revision 4)
@@ -0,0 +1,107 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: uifuncs.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Generic UI functions service
+
+
+from gi.repository import Gtk
+from gi.repository import Gio
+from gi.repository import Pango
+from gi.repository.GdkPixbuf import Pixbuf
+
+from .service import Service
+
+class UIFuncs(Service):
+ def initialize(self):
+ self.log.debug("Loading common UI Funcs service")
+ self.gui = self.app.get_service('GUI')
+ self.cb = self.app.get_service('Callbacks')
+ self.im = self.app.get_service('IM')
+
+
+ def get_label(self, text, xalign=0.5, yalign=0.5):
+ label = Gtk.Label()
+ label.set_selectable(False)
+ label.set_markup(text)
+ label.props.xalign = xalign
+ label.props.yalign = yalign
+ label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
+
+ return label
+
+
+ def get_separator(self, draw=False, expand=False):
+ separator = Gtk.SeparatorToolItem.new ()
+ separator.set_expand(expand)
+ separator.set_draw(draw)
+
+ return separator
+
+
+ def create_textview(self):
+ scrolledwindow = Gtk.ScrolledWindow()
+ scrolledwindow.set_hexpand(True)
+ scrolledwindow.set_vexpand(True)
+ scrolledwindow.set_shadow_type(Gtk.ShadowType.IN)
+ textview = Gtk.TextView()
+ textbuffer = textview.get_buffer()
+ textbuffer.set_text("This is some text inside of a Gtk.TextView. "
+ + "Select text and click one of the buttons 'bold', 'italic', "
+ + "or 'underline' to modify the text accordingly.")
+ scrolledwindow.add(textview)
+
+ return scrolledwindow
+
+
+ def get_box(self):
+ if Gtk.get_minor_version() < 14:
+ return Gtk.Alignment()
+ else:
+ return Gtk.Box()
+
+
+ def create_item(self, name, action, icon):
+ item = Gio.MenuItem.new(name, action)
+ if len(icon) > 0:
+ item.set_icon(Gio.ThemedIcon.new(icon))
+ #~ icon = self.im.get_themed_icon(icon)
+
+ return item
+
+
+ def create_action(self, name, callback=None, user_data=None):
+ action = Gio.SimpleAction.new(name, None)
+
+ if callback is None:
+ window = self.gui.get_widget('mainwindow')
+ action.connect('activate', self.cb.execute_action, user_data)
+ else:
+ action.connect('activate', callback, user_data)
+ return action
+
+
+ def message_dialog(self, head, body):
+ dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "%s" % head)
+ dialog.format_secondary_text("%s" % body)
+ dialog.run()
+ dialog.destroy()
+
+
+ def fullscreen(self, switch, do_fullscreen=None):
+ window = self.gui.get_widget('mainwindow')
+
+ if do_fullscreen is None:
+ #~ Get state from button:
+ toggle = self.gui.get_widget('tgbFullScreen')
+ do_fullscreen = toggle.get_active()
+
+ if do_fullscreen == True:
+ #~ window.fullscreen()
+ window.maximize()
+ else:
+ #~ window.unfullscreen()
+ window.unmaximize()
+
+
/basico/uifuncs.py
Property changes:
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Index: basico/uiapp.py
===================================================================
--- basico/uiapp.py (nonexistent)
+++ basico/uiapp.py (revision 4)
@@ -0,0 +1,122 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: uiapp.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Gtk.Application instance
+
+import os
+import sys
+import subprocess
+from datetime import datetime
+
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository.GdkPixbuf import Pixbuf
+from gi.repository import Pango
+from gi.repository import GLib
+from gi.repository import GObject
+from gi.repository import Gio
+
+from .service import Service
+from .window import GtkAppWindow
+from .log import get_logger
+from .env import FILE
+
+class UIApp(Gtk.Application):
+ """
+ """
+ def __init__(self, controller):
+ Gtk.Application.__init__(self,
+ application_id="net.t00mlabs.basico",
+ flags=Gio.ApplicationFlags.FLAGS_NONE)
+ GLib.set_application_name("Basico")
+ GLib.set_prgname('basico')
+ self.log = get_logger(self.__class__.__name__, FILE['LOG'])
+ self.controller = controller
+ #~ print (controller.list_services())
+ #~ print (dir(controller))
+ self.get_services()
+
+
+ def get_services(self):
+ self.gui = self.controller.get_service('GUI')
+ self.im = self.controller.get_service('IM')
+ self.cb = self.controller.get_service('Callbacks')
+
+
+ def do_activate(self):
+ self.window = GtkAppWindow(self)
+ self.window.connect("delete-event", self.gui.quit)
+ self.window.show()
+
+
+ def do_startup(self):
+ Gtk.Application.do_startup(self)
+
+ # show icons on the buttons
+ #~ settings = Gtk.Settings.get_default()
+ #~ DEPRECATED: settings.props.gtk_button_images = True
+
+ # actions that control the application: create, connect their
+ # signal to a callback method (see below), add the action to the
+ # application
+
+
+ def get_window(self):
+ return self
+
+
+ def get_controller(self):
+ return self.controller
+
+
+ def cb_hide_about(self, aboutdialog, user_data):
+ aboutdialog.destroy()
+
+
+ def cb_toggle_fullscreen(self, tgbutton, user_data=None):
+ if tgbutton.get_active():
+ self.window.fullscreen()
+ else:
+ self.window.unfullscreen()
+
+
+ def cb_show_about(self, *args):
+ DIR_ICONS = self.controller.get_var("ICONS")
+ CREDITS = self.controller.get_file("CREDITS")
+ rootwin = self.gui.get_widget('mainwindow')
+
+ aboutdialog = Gtk.AboutDialog()
+ aboutdialog.set_hide_titlebar_when_maximized(True)
+ icon_dlg = self.im.get_icon('basico', 96, 96)
+ applicense = Gtk.License(Gtk.License.GPL_3_0)
+ shortname = self.controller.get_app_info('short')
+ longname = self.controller.get_app_info('name')
+ authors = self.controller.get_app_info('authors')
+ #~ documenters = self.controller.get_app_info('documenters')
+ #~ aboutdialog.set_title("About %s" % longname)
+ aboutdialog.set_logo(icon_dlg)
+ aboutdialog.set_icon(icon_dlg)
+ #~ aboutdialog.add_credit_section("Icon 🙇", ["Single Unicode character (U+1F64x).\nSee Emoticons section from Unicode Standard version 6.0"])
+
+ #~ for line in open(CREDITS, 'r').readlines():
+ #~ data = line.split(';')
+ #~ aboutdialog.add_credit_section(data[0], [data[1]])
+ aboutdialog.set_comments(longname)
+ #~ aboutdialog.set_comments(longname)
+ #~ aboutdialog.set_program_name("%s\n(%s)" % (shortname, longname))
+ aboutdialog.set_version("0.1")
+ aboutdialog.set_copyright("Copyright \xa9 2016 Tomás Vírseda García")
+ aboutdialog.set_license_type(applicense)
+ aboutdialog.set_authors(authors)
+ #~ aboutdialog.set_documenters(documenters)
+ aboutdialog.set_website("http://t00mlabs.net")
+ aboutdialog.set_website_label("t00mlabs Website")
+ aboutdialog.set_title("")
+ aboutdialog.connect("response", self.cb_hide_about)
+ aboutdialog.set_transient_for(rootwin)
+ aboutdialog.set_modal(True)
+ #~ icon_app = Pixbuf.new_from_file_at_scale(DIR_ICONS + 'basico.png', 64, 64, True)
+ #~ aboutdialog.set_logo(icon_app)
+ aboutdialog.show()
Index: basico/i18n.py
===================================================================
--- basico/i18n.py (nonexistent)
+++ basico/i18n.py (revision 4)
@@ -0,0 +1,15 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# File: i18nb.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: i18n module
+
+import os
+import gettext
+
+base_dir = os.path.abspath(os.path.dirname(__file__))
+d = '/usr/local/share' if 'local' in base_dir.split('/') else '/usr/share'
+gettext.bindtextdomain('genxword', os.path.join(d, 'locale'))
+gettext.textdomain('basico')
+_ = gettext.gettext
Index: basico/menus.py
===================================================================
--- basico/menus.py (nonexistent)
+++ basico/menus.py (revision 4)
@@ -0,0 +1,127 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: menus.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Menus service
+
+
+from gi.repository import Gtk
+from gi.repository import Gio
+from gi.repository import Pango
+from gi.repository.GdkPixbuf import Pixbuf
+
+from .service import Service
+
+class Menus(Service):
+ def initialize(self):
+ self.get_services()
+ self.uiapp = self.gui.get_widget('uiapp')
+ self.window = self.gui.get_widget('mainwindow')
+
+
+ def get_services(self):
+ self.gui = self.app.get_service('GUI')
+ self.sap = self.app.get_service('SAP')
+ self.cb = self.app.get_service('Callbacks')
+ self.tasks = self.gui.get_service('Tasks')
+ self.im = self.gui.get_service('IM')
+
+
+ def create_popup_menu_by_task(self, task):
+ #~ self.log.debug(task)
+ menu = Gtk.Menu()
+ ICONS_DIR = self.get_var('ICONS')
+ sapnoteview = self.gui.get_widget('sapnoteview')
+
+ item = Gtk.ImageMenuItem()
+ icon = self.im.get_image_icon('task', 24, 24)
+ item.set_image(icon)
+ item.set_always_show_image(True)
+ item.set_label("Select all SAP Notes on this task")
+ item.connect("activate", self.select_by_task, task, True)
+ menu.append(item)
+
+ item = Gtk.ImageMenuItem()
+ icon = self.im.get_image_icon('task', 24, 24)
+ item.set_image(icon)
+ item.set_always_show_image(True)
+ item.set_label("Unselect all SAP Notes on this task")
+ item.connect("activate", self.select_by_task, task, False)
+ menu.append(item)
+
+ return menu
+
+
+ def select_by_task(self, menuitem, task, active):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnoteview.select_by_task(task, active)
+
+
+ def create_popup_menu_by_sapnote(self, sid):
+ sapnote = self.sap.get_node(sid)
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ component = sapnote['componentkey']
+ category = sapnote['category']
+ ICONS_DIR = self.get_var('ICONS')
+
+ menu = Gtk.Menu()
+
+ item = Gtk.ImageMenuItem()
+ icon = self.im.get_image_icon('component', 24, 24)
+ item.set_image(icon)
+ item.set_always_show_image(True)
+ item.set_label("Select all SAP Notes with component %s" % component)
+ item.connect("activate", self.select_by_component, component, True)
+ menu.append(item)
+
+ return menu
+
+
+ def create_popup_menu_by_component(self, component):
+ #~ self.log.debug(component)
+ menu = Gtk.Menu()
+ ICONS_DIR = self.get_var('ICONS')
+ sapnoteview = self.gui.get_widget('sapnoteview')
+
+ item = Gtk.ImageMenuItem()
+ icon = self.im.get_image_icon('component', 24, 24)
+ item.set_image(icon)
+ item.set_always_show_image(True)
+ item.set_label("Select all SAP Notes on this component")
+ item.connect("activate", self.select_by_component, component, True)
+ menu.append(item)
+
+ item = Gtk.ImageMenuItem()
+ icon = self.im.get_image_icon('component', 24, 24)
+ item.set_image(icon)
+ item.set_always_show_image(True)
+ item.set_label("Unselect all SAP Notes on this component")
+ item.connect("activate", self.select_by_component, component, False)
+ menu.append(item)
+
+ return menu
+
+
+ def select_by_component(self, menuitem, component, active):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnoteview.select_by_component(component, active)
+
+
+ def create_item(self, name, action, icon):
+ item = Gio.MenuItem.new(name, action)
+ item.set_icon(Gio.ThemedIcon.new(icon))
+ return item
+
+
+ def create_action(self, name, callback=None):
+ action = Gio.SimpleAction.new(name, None)
+
+ if callback is None:
+ window = self.gui.get_widget('mainwindow')
+ action.connect('activate', window.action_clicked)
+ else:
+ action.connect('activate', callback)
+ return action
+
+
Index: basico/__init__.py
===================================================================
--- basico/__init__.py (nonexistent)
+++ basico/__init__.py (revision 4)
@@ -0,0 +1,6 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# File: __init__.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: basico module
Index: basico/service.py
===================================================================
--- basico/service.py (nonexistent)
+++ basico/service.py (revision 4)
@@ -0,0 +1,142 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: service.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Service class
+
+import sys
+
+from .log import get_logger
+
+
+class Service(object):
+ """
+ Service class is the base class for the rest of main classes used in
+ the application.
+ Different modules (GUI, Database, Ask, etc...) share same methods
+ which is useful to start/stop them, simplify logging and, comunicate
+ each other easily.
+ """
+
+ def __init__(self, app=None):
+ """Initialize Service instance
+ @type app: Basico instance
+ @param app: current Basico instance reference
+ """
+ self.started = False
+
+
+ def is_started(self):
+ """Return True or False if service is running / not running
+ """
+ return self.started
+
+
+ def start(self, app, logname=None):
+ """Start service.
+ Use initialize for writting a custom init method
+ @type app: basico
+ @param app: basico Class pointer.
+ @type logname: string
+ @param logname: name of associated logger. It is used aswell to
+ identify configuration section name
+ """
+ self.started = True
+ self.app = app
+ logfile = self.app.get_file('LOG')
+ self.log = get_logger(logname, logfile)
+ self.config = self.app.get_config()
+ self.section = logname
+ self.init_section(logname)
+
+ try:
+ self.initialize()
+ self.log.debug("Service %s loaded" % logname)
+ except Exception as error:
+ self.log.error (self.get_traceback())
+
+
+ def get_var(self, name, scope='global'):
+ return self.app.get_var(name, scope)
+
+
+ def get_app_info(self, name):
+ return self.app.get_app_info(name)
+
+
+ def get_file(self, name):
+ return self.app.get_file(name)
+
+
+ def end(self):
+ """End service
+ Use finalize for writting a custom end method
+ """
+ self.started = False
+ try:
+ self.finalize()
+ except Exception as error:
+ self.log.error (self.get_traceback())
+
+
+ def initialize(self):
+ """Initialize service.
+ All clases derived from Service class must implement this method
+ """
+ pass
+
+
+ def finalize(self):
+ """Finalize service.
+ All clases derived from Service class must implement this method
+ """
+ pass
+
+
+ def get_config_value(self, key):
+ """Get value for a given param in section for this service
+ @type param: string
+ @param param: parameter name
+ """
+ self.config = self.app.get_config()
+ if self.config.has_section(self.section):
+ if self.config.has_option(self.section, key):
+ return self.config.get(self.section, key)
+
+ return None
+
+
+ def set_config_value(self, key, value):
+ """Set value for a given param in section for this service
+ @type param: string
+ @param param: parameter name
+ @type value: string
+ @param param: new value for this parameter
+ """
+ self.config[self.section][key] = value
+ self.log.debug("CONFIG[%s][%s] = %s" % (self.section, key, value))
+ self.save_config()
+
+
+ def init_section(self, section):
+ """Check if section exists in config. If not, create it"""
+ if not self.config.has_section(section):
+ self.config.add_section(section)
+ self.log.debug("CONFIG[%s] section created" % section)
+ self.save_config()
+
+
+ def save_config(self):
+ CONFIG_FILE = self.get_file('CNF')
+ with open(CONFIG_FILE, 'w') as configfile:
+ self.config.write(configfile)
+ #~ self.log.debug('Configuration file save')
+
+
+ def get_traceback(self):
+ return self.app.get_traceback()
+
+
+ def get_service(self, name):
+ return self.app.get_service(name)
/basico/service.py
Property changes:
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Index: basico/notify.py
===================================================================
--- basico/notify.py (nonexistent)
+++ basico/notify.py (revision 4)
@@ -0,0 +1,27 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: notify.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: notifications service
+
+import sys
+import gi
+gi.require_version('Notify', '0.7')
+from gi.repository import Notify
+
+from .service import Service
+
+
+class Notification(Service):
+ def initialize(self):
+ Notify.init('Basico')
+
+ def show(self, module, message, icon_name):
+ if sys.platform == 'win32':
+ # Windows does not support Notify
+ return
+
+ icon = "dialog-%s" % icon_name # information | question | warning | error
+ notification = Notify.Notification.new (module, message, icon)
+ notification.show ()
Index: basico/data/icons/wait.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/wait.png
===================================================================
--- basico/data/icons/wait.png (nonexistent)
+++ basico/data/icons/wait.png (revision 4)
/basico/data/icons/wait.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/project.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/project.png
===================================================================
--- basico/data/icons/project.png (nonexistent)
+++ basico/data/icons/project.png (revision 4)
/basico/data/icons/project.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/callbacks.py
===================================================================
--- basico/callbacks.py (nonexistent)
+++ basico/callbacks.py (revision 4)
@@ -0,0 +1,524 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: callbacks.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: UI and related callbacks service
+
+import os
+import csv
+import json
+
+import gi
+gi.require_version('Gtk', '3.0')
+gi.require_version('WebKit', '3.0')
+from gi.repository import Gtk
+from gi.repository import Gio
+from gi.repository import Pango
+from gi.repository.GdkPixbuf import Pixbuf
+from gi.repository import WebKit
+
+from .service import Service
+
+PROPKEYS = ['tasks', 'id', 'title', 'type', 'componentkey', 'componenttxt',
+ 'category', 'version', 'priority', 'language', 'releaseon',
+ 'bookmark']
+
+class Callback(Service):
+ def initialize(self):
+ self.log.debug("Loading UI Callbacks")
+ self.get_services()
+
+ def get_services(self):
+ self.gui = self.app.get_service('GUI')
+ self.uif = self.app.get_service("UIF")
+ self.sap = self.app.get_service('SAP')
+ self.tasks = self.app.get_service('Tasks')
+ self.alert = self.app.get_service('Notify')
+ self.stats = self.app.get_service('Stats')
+
+ def execute_action(self, *args):
+ action = args[0]
+ action_name = action.get_name()
+ try:
+ callback = "self.%s()" % action_name.replace('-', '_')
+ return eval(callback)
+ except Exception as error:
+ self.log.error(error)
+ self.log.error("Callback for action '%s' not registered" % action_name)
+ raise
+
+
+ def actions_browse(self, *args):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnotes = list(sapnoteview.get_selected_notes())
+ #~ sapnotes.sort()
+ try:
+ self.sap.browse_notes(sapnotes)
+ except: pass
+
+
+ def actions_other_delete(self, *args):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnotes = list(sapnoteview.get_selected_notes())
+ sapnotes.sort()
+ winroot = self.gui.get_widget('mainwinow')
+
+ dialog = Gtk.MessageDialog(winroot, 0, Gtk.MessageType.WARNING,
+ Gtk.ButtonsType.OK_CANCEL, "Are you sure?")
+ dialog.set_title("Deleting SAP Notes...")
+ dialog.set_modal(True)
+ dialog.set_transient_for(winroot)
+ dialog.format_secondary_text(
+ "These SAP Notes will be deleted:\n%s" % ', '.join(sapnotes))
+
+ response = dialog.run()
+ if response == Gtk.ResponseType.OK:
+ sapnotes = sapnoteview.get_selected_notes()
+ for sapnote in sapnotes:
+ self.sap.delete_sapnote(sapnote)
+ self.search_notes()
+ self.refresh_view()
+ self.alert.show('Delete', 'Selected SAP Notes deleted', 'information')
+ self.log.info("Selected SAP Notes deleted")
+ self.sap.save_notes()
+ elif response == Gtk.ResponseType.CANCEL:
+ self.alert.show('Delete', 'Delete action canceled by user', 'warning')
+ self.log.info("Delete action canceled by user")
+
+ dialog.destroy()
+
+ return response
+
+
+ def actions_manage_tasks(self, *args):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ try:
+ sapnotes = list(sapnoteview.get_selected_notes())
+ except:
+ sapnote = ''
+ self.tasks.show_window(sapnotes)
+
+
+ def actions_bookmark(self, *args):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnotes = list(sapnoteview.get_selected_notes())
+ view = sapnoteview.get_view()
+ self.sap.set_bookmark(sapnotes)
+ self.sap.save_notes()
+ self.refresh_view(view=view)
+ self.alert.show('Bookmarks', 'Selected SAP Notes bookmarked', 'information')
+
+
+ def actions_unbookmark(self, *args):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnotes = list(sapnoteview.get_selected_notes())
+ self.sap.set_no_bookmark(sapnotes)
+ self.sap.save_notes()
+ self.refresh_view(view='bookmarks')
+ self.alert.show('Bookmarks', 'Selected SAP Notes unbookmarked', 'information')
+
+
+ def actions_export_csv(self, *args):
+ rootwin = self.gui.get_widget('mainwndow')
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnotes = list(sapnoteview.get_selected_notes())
+ dialog = Gtk.FileChooserDialog("Save file", rootwin,
+ Gtk.FileChooserAction.SAVE,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
+ response = dialog.run()
+
+ if response == Gtk.ResponseType.OK:
+ export_path = dialog.get_filename()
+ writer = csv.writer(open(export_path, 'w'), delimiter=';', quoting=csv.QUOTE_ALL)
+ csvrow = []
+ for key in PROPKEYS:
+ csvrow.append(key)
+ writer.writerow(csvrow)
+ for sapnote in sapnotes:
+ csvrow = []
+ props = self.sap.get_node(sapnote)
+ for prop in PROPKEYS:
+ if prop == 'tasks':
+ tasks = ', '.join(props[prop])
+ csvrow.append(tasks)
+ else:
+ csvrow.append(props[prop])
+ writer.writerow(csvrow)
+ self.alert.show('Export', 'Selected SAP Notes exported successfully to CSV format', 'information')
+ self.log.info("Selected SAP Notes exported to CSV format: %s" % export_path)
+ else:
+ self.alert.show('Export', 'Export canceled by user', 'warning')
+ self.log.info("Export canceled by user")
+ dialog.destroy()
+
+
+ def actions_export_txt(self, *args):
+ rootwin = self.gui.get_widget('mainwndow')
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnotes = list(sapnoteview.get_selected_notes())
+ dialog = Gtk.FileChooserDialog("Save file", rootwin,
+ Gtk.FileChooserAction.SAVE,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
+ response = dialog.run()
+
+ if response == Gtk.ResponseType.OK:
+ export_path = dialog.get_filename()
+ fout = open(export_path, 'w')
+ for sapnote in sapnotes:
+ fout.write("%s\n" % sapnote)
+ self.alert.show('Export', 'Selected SAP Notes exported successfully to TXT format', 'information')
+ self.log.info("Selected SAP Notes exported to TXT format: %s" % export_path)
+ else:
+ self.alert.show('Export', 'Export canceled by user', 'warning')
+ self.log.info("Export canceled by user")
+ dialog.destroy()
+
+
+ def actions_export_bco(self, *args):
+ rootwin = self.gui.get_widget('mainwndow')
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnotes = list(sapnoteview.get_selected_notes())
+ dialog = Gtk.FileChooserDialog("Save file", rootwin,
+ Gtk.FileChooserAction.SAVE,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
+ response = dialog.run()
+
+ if response == Gtk.ResponseType.OK:
+ export_path = dialog.get_filename()
+ bag = {}
+ for sid in sapnotes:
+ sapnote = self.sap.get_node(sid)
+ bag[sid] = sapnote
+ self.sap.save_notes(export_path, bag)
+ self.alert.show('Export', 'Selected SAP Notes exported successfully to BCO format', 'information')
+ self.log.info("Selected SAP Notes exported to BCO format: %s" % export_path)
+ else:
+ self.alert.show('Export', 'Export canceled by user', 'warning')
+ self.log.info("Export canceled by user")
+ dialog.destroy()
+
+
+
+ def set_search_filter_key(self, key):
+ self.gui.set_key('cmbvalue', key)
+
+
+ def get_search_filter_key(self):
+ cmbvalue = self.gui.get_key('cmbvalue')
+
+
+ def set_search_term(self, term):
+ searchentry = self.gui.get_widget("stySearchInfo")
+ searchentry.set_text(term)
+
+
+ def search_notes(self, *args):
+ searchentry = self.gui.get_widget("stySearchInfo")
+ cmbvalue = self.gui.get_key('cmbvalue')
+ self.log.debug("Searching in %s" % cmbvalue)
+ try:
+ term = searchentry.get_text()
+ except:
+ term = ''
+ self.log.debug("Looking for '%s'" % term)
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnotes = self.sap.get_notes()
+ found = {}
+
+ if len(term) == 0:
+ self.current_notes = found = sapnotes
+ sapnoteview.populate(found)
+ sapnoteview.collapse()
+ return
+
+ if cmbvalue == 'search':
+ for sid in sapnotes:
+ if term.upper() in sapnotes[sid]['title'].upper():
+ found[sid] = sapnotes[sid]
+ elif cmbvalue == 'project':
+ for sid in sapnotes:
+ try:
+ projects = sapnotes[sid]['projects']
+ for project in projects:
+ if term.upper() in project.upper():
+ found[sid] = sapnotes[sid]
+ except: pass
+ elif cmbvalue == 'task':
+ #~ self.log.debug(sapnotes)
+ for sid in sapnotes:
+ try:
+ tasks = sapnotes[sid]['tasks']
+ for task in tasks:
+ if term.upper() in task.upper():
+ found[sid] = sapnotes[sid]
+ except: pass
+ elif cmbvalue == 'component':
+ for sid in sapnotes:
+ if term.upper() in sapnotes[sid]['componentkey'].upper():
+ found[sid] = sapnotes[sid]
+ #~ if term.upper() in sapnotes[sid]['componenttxt'].upper():
+ #~ found[sid] = sapnotes[sid]
+ elif cmbvalue == 'category':
+ for sid in sapnotes:
+ if term.upper() in sapnotes[sid]['category'].upper():
+ found[sid] = sapnotes[sid]
+ elif cmbvalue == 'type':
+ for sid in sapnotes:
+ if term.upper() in sapnotes[sid]['type'].upper():
+ found[sid] = sapnotes[sid]
+ elif cmbvalue == 'version':
+ for sid in sapnotes:
+ if term.upper() in sapnotes[sid]['version'].upper():
+ found[sid] = sapnotes[sid]
+ elif cmbvalue == 'id':
+ for sid in sapnotes:
+ if term in sid:
+ found[sid] = sapnotes[sid]
+ #~ elif cmbvalue == 'released':
+ #~ for sid in sapnotes:
+ #~ self.log.debug(sapnotes[sid]['reldate'].upper())
+ #~ self.log.debug(sapnotes[sid]['releaseon'].upper())
+ #~ if term.upper() in sapnotes[sid]['releaseon'].upper():
+ #~ found[sid] = sapnotes[sid]
+ #~ Exact match
+ #~ sid = "0"*(10 - len(term)) + term
+ #~ self.log.debug("SAP Note id: %s" % sid)
+ #~ found[sid] = sapnotes[sid]
+ self.log.info("Term: '%s' (%d results)" % (term, len(found)))
+ #~ if len(found) == 0:
+ #~ found['XXXXXXXXXX'] = None
+ self.current_notes = found
+ self.log.debug("Current Notes: %d" % len(self.current_notes))
+ sapnoteview.populate(found)
+ #~ sapnoteview.expand()
+ #~ self.refresh_view()
+
+
+ def import_notes(self, entry):
+ ntbimport = self.gui.get_widget('ntbAddSAPNotes')
+ imptype = ntbimport.get_current_page() # 0 -> Download, 1 -> Import from file
+ #~ self.log.debug(imptype)
+ if imptype == 0:
+ self.import_notes_from_sapnet()
+ elif imptype == 1:
+ self.import_notes_from_file()
+
+
+ def import_notes_from_file(self):
+ notebook = self.gui.get_widget('notebook')
+ winroot = self.gui.get_widget('mainwinow')
+ filechooser = self.gui.get_widget('fcwImportNotes')
+ import_path = filechooser.get_filename()
+ #~ self.log.debug(import_path)
+ try:
+ with open(import_path, 'r') as fp:
+ bag = json.load(fp)
+ self.sap.import_sapnotes(bag)
+ self.log.info ("Imported %d notes from %s" % (len(bag), import_path))
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ self.current_notes = bag
+ sapnoteview.populate(bag)
+ self.sap.save_notes()
+ self.refresh_view()
+ switch = self.gui.get_widget('schSelectNotesAllNone')
+ sapnoteview.select_all_none(switch, True)
+ sapnoteview.select_all_none(switch, False)
+ sapnoteview.expand_all()
+ except Exception as error:
+ self.alert.show('Import', 'Nothing imported', 'error')
+ self.log.warning("SAP Notes not found")
+
+
+
+ def import_notes_from_sapnet(self):
+ notebook = self.gui.get_widget('notebook')
+ winroot = self.gui.get_widget('mainwinow')
+ sapnotes = []
+ bag = set()
+ txtnotes = self.gui.get_widget('txtSAPNotes')
+ textbuffer = txtnotes.get_buffer()
+ istart, iend = textbuffer.get_bounds()
+ lines = textbuffer.get_text(istart, iend, False)
+
+ lines = lines.replace(',', '\n')
+ sapnotes.extend(lines.split('\n'))
+
+ for sapnote in sapnotes:
+ if len(sapnote.strip()) > 0:
+ bag.add(sapnote.strip())
+
+ self.log.debug("%d SAP Notes to be downloaded: %s" % (len(bag), ', '.join(list(bag))))
+
+ driver = self.sap.connect()
+ if driver is None:
+ dialog = Gtk.MessageDialog(winroot, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "Task canceled")
+ dialog.format_secondary_text("No suitable webdriver found. Check preferences.")
+ dialog.run()
+ dialog.destroy()
+
+ resnotes = {}
+ for sapnote in bag:
+ rc = self.sap.download_note(sapnote, driver)
+ resnotes[sapnote] = rc
+ self.sap.build_stats()
+ self.sap.logout(driver)
+
+ textbuffer.set_text("")
+ self.log.info("Task completed.")
+ notebook.set_current_page(0)
+ dialog = Gtk.MessageDialog(winroot, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "Task completed")
+ msgrc = ""
+ ko = 0
+ ok = 0
+ for sapnote in resnotes:
+ rc = resnotes[sapnote]
+ if rc:
+ ok += 1
+ else:
+ ko += 1
+ msgrc += "Downloaded: %d\nErroneus: %d" % (ok, ko)
+ self.alert.show('Download', msgrc, 'information')
+ dlbag = {}
+ mysapnotes = self.sap.get_notes()
+ erroneus = set()
+ for sid in bag:
+ sid = "0"*(10 - len(sid)) + sid
+ try:
+ dlbag[sid] = mysapnotes[sid]
+ except Exception as error:
+ self.log.error(error)
+ erroneus.add(sid)
+ #~ self.log.error(erroneus)
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ self.current_notes = dlbag
+ sapnoteview.populate(dlbag)
+ self.refresh_view()
+ sapnoteview.expand_all()
+
+
+ def stop_dl_notes(self, *args):
+ notebook = self.gui.get_widget('notebook')
+ txtSAPNotes = self.gui.get_widget('txtSAPNotes')
+ buffer = txtSAPNotes.get_buffer()
+ buffer.set_text("")
+ notebook.set_current_page(0)
+ self.refresh_view()
+ self.alert.show('Download', 'Action canceled by user', 'warning')
+
+ def refresh_and_clear_view(self, *args):
+ switch = self.gui.get_widget('schSelectNotesAllNone')
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ self.set_search_filter_key('search')
+ self.set_search_term('')
+ self.search_notes()
+ self.refresh_view()
+ sapnoteview.select_all_none(switch, False)
+ sapnoteview.collapse()
+
+ def refresh_view(self, action=None, callback=None, view=None):
+ window = self.gui.get_widget('mainwindow')
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ switch = self.gui.get_widget('schExpandCollapse')
+ active = switch.get_active()
+ if view is not None:
+ viewlabel = self.gui.get_widget('lblViewCurrent')
+ name = "<span size='20000'><b>%-10s</b></span>" % view.capitalize()
+ viewlabel.set_markup(name)
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ old_view = sapnoteview.get_view()
+ sapnoteview.set_view(view)
+ #~ self.log.debug("switching view from %s to %s" % (old_view, view))
+ #~ sapnoteview.populate(self.current_notes)
+ self.search_notes()
+ #~ sapnoteview.expand_collapse(switch, active)
+ sapnoteview.set_view(view)
+ window.show_home_page()
+
+
+ def setup_menu_actions(self):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ view = sapnoteview.get_view()
+ #~ self.log.debug("View: %s" % view)
+ ### ACTIONS POPOVER
+ app = self.gui.get_app()
+
+ ## Action Menu
+ actions_menu = Gio.Menu()
+
+ #~ # Browse SAP Notes
+ actions_menu.append_item(self.uif.create_item('Browse SAP Note(s)', 'app.actions-browse', 'browse'))
+ app.add_action(self.uif.create_action("actions-browse"))
+
+ if view == 'bookmarks':
+ #~ Unbookmark SAP Note(s) items
+ actions_menu.append_item(self.uif.create_item('Unbookmark SAP Note(s)', 'app.actions-unbookmark', 'bookmark'))
+ app.add_action(self.uif.create_action("actions-unbookmark"))
+ else:
+ #~ Bookmark SAP Note(s) items
+ actions_menu.append_item(self.uif.create_item('Bookmark SAP Note(s)', 'app.actions-bookmark', 'bookmark'))
+ app.add_action(self.uif.create_action("actions-bookmark"))
+
+ # Manage task
+ actions_menu.append_item(self.uif.create_item('Manage tasks', 'app.actions-manage-tasks', 'tasks'))
+ app.add_action(self.uif.create_action("actions-manage-tasks"))
+
+ # Export submenu
+ actions_export_submenu = Gio.Menu()
+ #~ Export to CSV
+ actions_export_submenu.append_item(self.uif.create_item('Export as CSV', 'app.actions-export-csv', 'document-save'))
+ app.add_action(self.uif.create_action("actions-export-csv"))
+ #~ Export to TXT
+ actions_export_submenu.append_item(self.uif.create_item('Export to plaint text', 'app.actions-export-txt', 'document-save'))
+ app.add_action(self.uif.create_action("actions-export-txt"))
+ #~ Export to BCO
+ actions_export_submenu.append_item(self.uif.create_item('Export as Basico Package Object (BCO)', 'app.actions-export-bco', 'document-save'))
+ app.add_action(self.uif.create_action("actions-export-bco"))
+ actions_menu.append_submenu('Export', actions_export_submenu)
+ #~ actions_menu.append_section('Export', actions_export_submenu)
+
+ # Refresh SAP Notes
+ #~ actions_menu.append_item(self.uif.create_item('Refresh selected SAP Notes', 'app.actions-other-refresh', 'refresh'))
+ #~ app.add_action(self.uif.create_action("actions-other-refresh"))
+
+ # Delete SAP Notes
+ actions_menu.append_item(self.uif.create_item('Delete selected SAP Notes', 'app.actions-other-delete', 'delete'))
+ app.add_action(self.uif.create_action("actions-other-delete"))
+
+ # MnuButton valid with any modern version of Gtk (?> 3.10)
+ btnactions = self.gui.get_widget('mnuBtnActions')
+ btnactions.set_always_show_image(True)
+ btnactions.set_property("use-popover", True)
+ btnactions.set_menu_model(actions_menu)
+
+
+ def apply_preferences(self, *args):
+ self.sap.apply_preferences()
+ notebook = self.gui.get_widget('notebook')
+ notebook.set_current_page(0)
+ self.refresh_view(view='tasks')
+ self.alert.show('Settings', 'SAP preferences saved', 'information')
+
+
+ def update_components_stats(self, *args):
+ statsviewer = self.gui.get_widget('scrStatsViewer')
+ view = WebKit.WebView()
+ chart = self.stats.build_pie_maincomp()
+ view.load_string(chart, 'text/html', 'UTF-8','/')
+ self.gui.swap_widget(statsviewer, view)
+
+ def update_categories_stats(self, *args):
+ statsviewer = self.gui.get_widget('scrStatsViewer')
+ view = WebKit.WebView()
+ chart = self.stats.build_pie_categories()
+ view.load_string(chart, 'text/html', 'UTF-8','/')
+ self.gui.swap_widget(statsviewer, view)
+
+
+ def test(self, *args):
+ self.log.debug(args)
+
+
Index: basico/log.py
===================================================================
--- basico/log.py (nonexistent)
+++ basico/log.py (revision 4)
@@ -0,0 +1,32 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: log.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: log service
+
+from os.path import sep as SEP
+import logging
+
+
+def get_logger(name, LOG_FILE):
+ """Returns a new logger with personalized.
+ @param name: logger name
+ """
+ log = logging.getLogger(name)
+ log.setLevel(logging.DEBUG)
+
+ ## Redirect log to stdout
+ formatter = logging.Formatter("%(levelname)7s | %(lineno)4d |%(name)15s | %(asctime)s | %(message)s")
+ ch = logging.StreamHandler() # Create console handler and set level to debug
+ ch.setLevel(logging.DEBUG) # Set logging devel
+ ch.setFormatter(formatter) # add formatter to console handler
+ log.addHandler(ch) # add console handler to logger
+
+ # Redirect log to file
+ fh = logging.FileHandler(LOG_FILE)
+ fh.setFormatter(formatter)
+ fh.setLevel(logging.DEBUG) # Set logging devel
+ log.addHandler(fh) # add file handler to logger
+
+ return log
/basico/log.py
Property changes:
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Index: basico/plugins.py
===================================================================
--- basico/plugins.py (nonexistent)
+++ basico/plugins.py (revision 4)
@@ -0,0 +1,161 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: hooks.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: a simple plugin system based on hooks
+
+import os
+
+import pkg_resources
+
+from .service import Service
+
+ENTRYPOINT = 'basico.plugins'
+
+class Plugins(Service):
+ def initialize(self):
+ self.blacklist = set()
+ self.rebuild_plugins()
+ self.plugins = self.init_plugins()
+
+
+ def __len__(self):
+ return len(self.plugins)
+
+
+ def get_services(self):
+ pass
+
+
+ def rebuild_local_plugins(self):
+ #~ print GPATH['PLUGINS']
+ for plugindir in os.listdir(LPATH['PLUGINS']):
+ setupdir = LPATH['PLUGINS'] + '/' + plugindir
+ setupfile = LPATH['PLUGINS'] + '/' + plugindir + '/setup.py'
+ #~ print (setupfile)
+ if os.path.exists(setupfile):
+ cmd = "cd %s; python %s develop --install-dir .. -m" % (setupdir, setupfile)
+ (exitstatus, outtext) = commands.getstatusoutput(cmd)
+ if exitstatus != 0:
+ self.log.warning( "Plugin '%s' not available. Error: %d" % (plugindir, exitstatus) )
+ self.log.warning( outtext )
+
+
+ def rebuild_plugins(self):
+ DIR_PLUGINS = self.get_var('PLUGINS', 'local')
+ self.log.debug("Looking for plugins in: %s" % DIR_PLUGINS)
+ for plugindir in os.listdir(DIR_PLUGINS):
+ setupdir = DIR_PLUGINS + '/' + plugindir
+ setupfile = DIR_PLUGINS + '/' + plugindir + '/setup.py'
+ if os.path.exists(setupfile):
+ cmd = "cd %s; python3 %s develop --install-dir .. -m > /dev/null 2>&1" % (setupdir, setupfile)
+ exitstatus = os.system(cmd)
+ #~ (exitstatus, outtext) = commands.getstatusoutput(cmd)
+ self.log.debug(exitstatus)
+ if exitstatus != 0:
+ self.log.warning( "Plugin '%s' not available. Error: %d" % (plugindir, exitstatus) )
+ #~ self.log.warning( outtext )
+
+
+ def init_plugins(self):
+ """
+ Load local and system hooks.
+ """
+ DIR_PLUGINS = self.get_var('PLUGINS')
+ plugins = {}
+
+ # Load local plugins
+ pkg_resources.working_set.add_entry(DIR_PLUGINS)
+ pkg_env = pkg_resources.Environment([DIR_PLUGINS])
+ loaded = 0
+ for name in pkg_env:
+ egg = pkg_env[name][0]
+ egg.activate()
+ modules = []
+ for name in egg.get_entry_map(ENTRYPOINT):
+ try:
+ entry_point = egg.get_entry_info(ENTRYPOINT, name)
+ cls = entry_point.load()
+ cls.app = self.app
+ #~ print dir(cls)
+ if not hasattr(cls, 'capabilities'):
+ cls.capabilities = []
+ instance = cls()
+ for c in cls.capabilities:
+ plugins.setdefault(c, []).append(instance)
+ loaded = loaded + 1
+ except Exception as error:
+ self.log.warning( "Hook '%s': %s" % (name, error))
+ raise
+
+ self.log.debug("Plugin system initialited: %d plugins loaded" % loaded)
+ return plugins
+
+
+ def get_plugin_by_key(self, key):
+ for plugin in self.get_all_plugins():
+ if plugin.key == key:
+ return plugin
+
+ return None
+
+
+ def get_plugins_by_capability(self, capability):
+ try:
+ plist = []
+ for plugin in self.plugins.get(capability, []):
+ active = self.get_active(plugin.key)
+ if active:
+ plist.append(plugin)
+ return plist
+ #~ return self.plugins.get(capability, [])
+ except Exception as error:
+ self.log.error( error )
+
+
+ def get_all_plugins(self):
+ result = set()
+ for p in self.plugins.itervalues():
+ for plugin in p:
+ result.add(plugin)
+ return list(result)
+
+
+ def get_plugin_path(self, plugin_key):
+ DIR_PLUGINS = self.get_var('PLUGINS')
+ return DIR_PLUGINS + '/' + plugin_key
+
+
+ def get_active(self, plugin_key):
+ mark = self.get_disabled_mark(plugin_key)
+ if os.path.exists(mark):
+ return False
+ else:
+ return True
+
+
+ def disable(self, plugin_key):
+ try:
+ mark = self.get_disabled_mark(plugin_key)
+ open(mark, 'w').close()
+ self.blacklist.add(plugin_key)
+ except:
+ pass
+
+
+ def enable(self, plugin_key):
+ mark = self.get_disabled_mark(plugin_key)
+ try:
+ os.remove(mark)
+ self.blacklist.remove(plugin_key)
+ except:
+ pass
+
+
+ def get_disabled_mark(self, plugin_key):
+ return self.get_plugin_path(plugin_key) + '/DISABLED'
+
+
+ def get_blacklist(self):
+ return self.blacklist
/basico/plugins.py
Property changes:
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Index: basico/data/desktop/basico.desktop
===================================================================
--- basico/data/desktop/basico.desktop (nonexistent)
+++ basico/data/desktop/basico.desktop (revision 4)
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Encoding=UTF-8
+Version=1.0
+Type=Application
+Terminal=false
+Exec=basico
+Name=basico
+Comment=SAP Notes manager for SAP Consultants
+Icon=basico
+Categories=GTK;GNOME;Utility;
+StartupNotify=True
Index: basico/data/icons/bookmarks.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/bookmarks.png
===================================================================
--- basico/data/icons/bookmarks.png (nonexistent)
+++ basico/data/icons/bookmarks.png (revision 4)
/basico/data/icons/bookmarks.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/basico.xcf
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/basico.xcf
===================================================================
--- basico/data/icons/basico.xcf (nonexistent)
+++ basico/data/icons/basico.xcf (revision 4)
/basico/data/icons/basico.xcf
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/unbookmark.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/unbookmark.png
===================================================================
--- basico/data/icons/unbookmark.png (nonexistent)
+++ basico/data/icons/unbookmark.png (revision 4)
/basico/data/icons/unbookmark.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/annotation.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/annotation.png
===================================================================
--- basico/data/icons/annotation.png (nonexistent)
+++ basico/data/icons/annotation.png (revision 4)
/basico/data/icons/annotation.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/export-basico.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/export-basico.png
===================================================================
--- basico/data/icons/export-basico.png (nonexistent)
+++ basico/data/icons/export-basico.png (revision 4)
/basico/data/icons/export-basico.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/preferences.py
===================================================================
--- basico/preferences.py (nonexistent)
+++ basico/preferences.py (revision 4)
@@ -0,0 +1,21 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: preferences.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Preferences service
+
+
+from gi.repository import Gtk
+from gi.repository import Gio
+from gi.repository import Pango
+from gi.repository.GdkPixbuf import Pixbuf
+
+from .service import Service
+
+class Preferences(Service):
+ def initialize(self):
+ self.sap = self.get_service('SAP')
+ self.gui = self.app.get_service('GUI')
+
+ self.uiapp = self.gui.get_widget('uiapp')
Index: basico/data/icons/edit.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/edit.png
===================================================================
--- basico/data/icons/edit.png (nonexistent)
+++ basico/data/icons/edit.png (revision 4)
/basico/data/icons/edit.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/browser.py
===================================================================
--- basico/browser.py (nonexistent)
+++ basico/browser.py (revision 4)
@@ -0,0 +1,425 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: browser.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Browse module
+#~ Source code borrowed from:
+#~ https://raw.githubusercontent.com/jaka/browser2/master/browser2
+#~ https://github.com/jaka
+#~ but adapted to Gtk3
+
+from gettext import gettext as _
+
+from gi.repository import GObject
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import Pango
+from gi.repository import WebKit
+
+
+class BrowserPage(WebKit.WebView):
+
+ def __init__(self):
+ WebKit.WebView.__init__(self)
+ settings = self.get_settings()
+ settings.set_property('enable-developer-extras', True)
+ settings.set_property('enable-default-context-menu', True)
+
+ settings.set_property('default-encoding', 'utf-8')
+ settings.set_property('enable-private-browsing', True)
+ settings.set_property('enable-html5-local-storage', True)
+
+ # disable plugins, like Adobe Flash and Java
+ settings.set_property('enable-plugins', True)
+
+ # scale other content besides from text as well
+ self.set_full_content_zoom(True)
+
+class TabLabel(Gtk.HBox):
+ """A class for Tab labels"""
+
+ __gsignals__ = {
+ 'close': (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_OBJECT,))
+ }
+
+ def __init__(self, title, scrolled_window):
+ """initialize the tab label"""
+
+ Gtk.HBox.__init__(self, False, 4)
+ self.title = title
+ self.scrolled_window = scrolled_window
+ self.label = Gtk.Label(title)
+ self.label.props.max_width_chars = 30
+ #~ self.label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
+ self.label.set_alignment(0.0, 0.5)
+
+ close_image = Gtk.Image.new_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU)
+ close_button = Gtk.Button()
+ close_button.set_relief(Gtk.ReliefStyle.NONE)
+ close_button.set_image(close_image)
+ close_button.connect('clicked', lambda x: self.emit('close', self.scrolled_window))
+
+ self.pack_start(self.label, True, True, 0)
+ self.pack_start(close_button, False, False, 0)
+
+ self.connect('style-set', self._tab_label_style_set)
+
+ def _tab_label_style_set(self, tab_label, style):
+ try:
+ context = tab_label.get_pango_context()
+ metrics = context.get_metrics(tab_label.style.font_desc, context.get_language())
+ char_width = metrics.get_approximate_digit_width()
+ (width, height) = Gtk.icon_size_lookup(Gtk.ICON_SIZE_MENU)
+ tab_label.set_size_request(10 * Pango.PIXELS(char_width) + 2 * width,
+ Pango.PIXELS(metrics.get_ascent() + metrics.get_descent()) + 4)
+ except: pass
+
+class ContentPane(Gtk.Notebook):
+
+ __gsignals__ = {
+ 'focus-entry': (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()),
+ 'update-title': (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_OBJECT, GObject.TYPE_STRING, GObject.TYPE_STRING)),
+ }
+
+ def __init__(self, app):
+ """initialize the content pane"""
+ Gtk.Notebook.__init__(self)
+ self.app = app
+ self.props.scrollable = True
+ self.set_show_tabs(True)
+ self.popup_enable()
+ #~ self.set_property('homogeneous', True)
+ self.set_show_border(True)
+ self.connect('switch-page', self._switch_page)
+
+ self.show_all()
+ self._hovered_uri = None
+
+ def new_tab(self, url = None, show_new = True, web_view = None):
+ """Creates a new page in a new tab"""
+
+ if web_view:
+ browser = web_view
+ else:
+ browser = BrowserPage()
+
+ scrolled_window = Gtk.ScrolledWindow()
+ scrolled_window.props.hscrollbar_policy = Gtk.PolicyType.AUTOMATIC
+ scrolled_window.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC
+ scrolled_window.title = None
+ scrolled_window.url = None
+
+ browser.connect('title-changed', self._title_changed, scrolled_window)
+ browser.connect('load-committed', self._url_changed, scrolled_window)
+ browser.connect('load-finished', self._load_finished, scrolled_window)
+ browser.connect('hovering-over-link', self._hovering_over_link)
+
+ # disable file chooser
+ #~ browser.connect('run-file-chooser', returntrue)
+
+ # disable file transfering (downloading)
+ #~ browser.connect('download-requested', returntrue)
+
+ # disable printing
+ #~ browser.connect('print-requested', returntrue)
+
+ browser.connect('create-web-view', self._new_web_view_request)
+ browser.connect('button-press-event', self._on_button_press_event)
+
+ scrolled_window.add(browser)
+ scrolled_window.show_all()
+
+ label = TabLabel(url, scrolled_window)
+ label.connect('close', self.close_tab)
+ label.show_all()
+
+ new_tab_number = self.append_page(scrolled_window, label)
+ #~ self.set_tab_label_packing(scrolled_window, False, True, Gtk.PACK_START)
+ self.set_tab_label(scrolled_window, label)
+
+ #~ self.set_show_tabs(self.get_n_pages() > 1)
+ self.show_all()
+
+ if url:
+ scrolled_window.url = url
+ browser.load_uri(url)
+ else:
+ scrolled_window.title = "New Tab"
+ scrolled_window.url = ""
+ label.label.set_label("New Tab")
+
+ if show_new:
+ self.set_current_page(new_tab_number)
+ if not url:
+ self.emit('focus-entry')
+
+
+ def close_tab(self, label, scrolled_window):
+ page_num = self.page_num(scrolled_window)
+ if page_num != -1:
+ browser = scrolled_window.get_child()
+ browser.destroy()
+ self.remove_page(page_num)
+ #~ self.set_show_tabs(self.get_n_pages() > 1)
+
+ def close_current_tab(self):
+ if (self.get_n_pages() > 1):
+ self.close_tab(None, self.get_nth_page(self.get_current_page()))
+
+ def go_back(self):
+ child = self.get_nth_page(self.get_current_page())
+ view = child.get_child()
+ view.go_back()
+
+ def go_forward(self):
+ child = self.get_nth_page(self.get_current_page())
+ view = child.get_child()
+ view.go_forward()
+
+ def refresh(self):
+ child = self.get_nth_page(self.get_current_page())
+ view = child.get_child()
+ view.reload()
+
+ def zoom_in(self):
+ """Zoom into the page"""
+ child = self.get_nth_page(self.get_current_page())
+ view = child.get_child()
+ view.zoom_in()
+
+ def zoom_out(self):
+ """Zoom out of the page"""
+ child = self.get_nth_page(self.get_current_page())
+ view = child.get_child()
+ view.zoom_out()
+
+ def load (self, text):
+ """Load the given uri in the current web view"""
+
+ child = self.get_nth_page(self.get_current_page())
+ view = child.get_child()
+ view.open(text)
+
+ def _on_button_press_event(self, widget, event):
+ if event.type == Gdk.EventType.BUTTON_PRESS and self._hovered_uri:
+ if event.button == 2:
+ self.new_tab(self._hovered_uri, False)
+ return True
+
+ def _hovering_over_link(self, view, title, uri):
+ self._hovered_uri = uri
+
+ def _switch_page(self, notebook, page, page_num):
+ scrolled_window = self.get_nth_page(page_num)
+ self._update_app(scrolled_window)
+
+ def _new_web_view_request(self, web_view, web_frame):
+ view = BrowserPage()
+ self.new_tab(web_view = view)
+ return view
+
+ def _get_title(self, web_view):
+ title = web_view.get_title()
+ if not title:
+ frame = web_view.get_main_frame()
+ title = frame.props.title
+ if not title:
+ title = frame.get_uri()
+ return title
+
+ def _load_finished(self, web_view, frame, scrolled_window):
+ scrolled_window.title = self._get_title(web_view)
+ #~ print (scrolled_window.title)
+ scrolled_window.url = frame.get_uri()
+ if web_view.get_can_focus():
+ web_view.grab_focus()
+ try:
+ # FIXME: this only MUST BE executed when load http://launchpad.support.sap.com
+ sap = self.app.get_service('SAP')
+ SUSER = sap.get_config_value('CNF_SAP_SUser')
+ SPASS = sap.get_config_value('CNF_SAP_SPass')
+ web_view.execute_script("document.getElementById('j_username').value='%s';" % SUSER)
+ web_view.execute_script("document.getElementById('j_password').value='%s';" % SPASS)
+ web_view.execute_script("document.getElementById('logOnFormSubmit').click();")
+ except Exception as error:
+ print(error)
+
+
+ def _title_changed(self, web_view, frame, title, scrolled_window):
+ scrolled_window.title = title
+ scrolled_window.url = frame.get_uri()
+ self._update_title(scrolled_window)
+
+ def _url_changed(self, web_view, frame, scrolled_window):
+ scrolled_window.title = self._get_title(web_view)
+ scrolled_window.url = frame.get_uri()
+ self._update_title(scrolled_window)
+
+ def _update_title(self, scrolled_window):
+ label = self.get_tab_label(scrolled_window)
+ label.label.set_label(scrolled_window.title)
+ if self.page_num(scrolled_window) == self.get_current_page():
+ self._update_app(scrolled_window)
+
+ def _update_app(self, scrolled_window):
+ title, url = scrolled_window.title, scrolled_window.url
+ self.emit('update-title', scrolled_window.get_child(), title, url)
+
+class WebToolbar(Gtk.Toolbar):
+
+ __gsignals__ = {
+ "go-back-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()),
+ "go-forward-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()),
+ "go-refresh-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()),
+ "go-home-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()),
+ "zoom-in-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()),
+ "zoom-out-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()),
+ "load-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)),
+ "new-tab-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ())
+ }
+
+ def __init__(self, location_enabled=True, toolbar_enabled=True):
+ Gtk.Toolbar.__init__(self)
+
+ self.set_style(Gtk.ToolbarStyle.ICONS)
+ self.set_icon_size(Gtk.IconSize.SMALL_TOOLBAR)
+
+ self.back_button = Gtk.ToolButton(Gtk.STOCK_GO_BACK)
+ self.forward_button = Gtk.ToolButton(Gtk.STOCK_GO_FORWARD)
+ self.refresh_button = Gtk.ToolButton(Gtk.STOCK_REFRESH)
+ self.home_button = Gtk.ToolButton(Gtk.STOCK_HOME)
+
+ self.back_button.connect('clicked', lambda x: self.emit('go-back-requested'))
+ self.forward_button.connect('clicked', lambda x: self.emit('go-forward-requested'))
+ self.refresh_button.connect('clicked', lambda x: self.emit('go-refresh-requested'))
+ self.home_button.connect('clicked', lambda x: self.emit('go-home-requested'))
+
+ self.add(self.back_button)
+ self.add(self.forward_button)
+ self.add(self.refresh_button)
+ self.add(self.home_button)
+
+ if True:
+ self.add(Gtk.SeparatorToolItem())
+ self.entry = Gtk.Entry()
+ self.entry.connect('activate', lambda x: self.emit('load-requested', self.entry.props.text))
+ entry_item = Gtk.ToolItem()
+ entry_item.set_expand(True)
+ entry_item.add(self.entry)
+ self.entry.show()
+ self.insert(entry_item, -1)
+ entry_item.show()
+
+ self.add(Gtk.SeparatorToolItem())
+ self.zoom_in_button = Gtk.ToolButton(Gtk.STOCK_ZOOM_IN)
+ self.zoom_out_button = Gtk.ToolButton(Gtk.STOCK_ZOOM_OUT)
+
+ self.zoom_in_button.connect('clicked', lambda x: self.emit('zoom-in-requested'))
+ self.zoom_out_button.connect('clicked', lambda x: self.emit('zoom-out-requested'))
+
+ self.add(self.zoom_in_button)
+ self.add(self.zoom_out_button)
+
+ if True:
+ self.add(Gtk.SeparatorToolItem())
+ self.addTabButton = Gtk.ToolButton(Gtk.STOCK_ADD)
+ self.addTabButton.connect('clicked', lambda x: self.emit('new-tab-requested'))
+ self.insert(self.addTabButton, -1)
+ self.addTabButton.show()
+
+class WebBrowser(Gtk.Box):
+
+ def __init__(self, app):
+ Gtk.Box.__init__(self)
+ self.app = app
+ self.start_url = 'https://launchpad.support.sap.com'
+
+ self.toolbar = WebToolbar()
+ self.content_tabs = ContentPane(app)
+
+ self.content_tabs.connect('focus-entry', lambda x: self.toolbar.entry.grab_focus())
+ self.content_tabs.connect('update-title', self._update_title)
+
+ self.toolbar.connect('new-tab-requested', lambda x: self.content_tabs.new_tab(None, True))
+ self.toolbar.connect('go-back-requested', lambda x: self.content_tabs.go_back())
+ self.toolbar.connect('go-forward-requested', lambda x: self.content_tabs.go_forward())
+ self.toolbar.connect('go-refresh-requested', lambda x: self.content_tabs.refresh())
+ self.toolbar.connect('go-home-requested', lambda x: self.content_tabs.load(self.start_url))
+ self.toolbar.connect('zoom-in-requested', lambda x: self.content_tabs.zoom_in())
+ self.toolbar.connect('zoom-out-requested', lambda x: self.content_tabs.zoom_out())
+ self.toolbar.connect('load-requested', self.load_requested)
+
+ vbox = Gtk.VBox(spacing = 0)
+ vbox.pack_start(self.toolbar, False, False, False)
+ vbox.pack_start(self.content_tabs, True, True, True)
+ self.pack_start(vbox, True, True, True)
+ #~ self.connect('destroy', self.quit)
+ #~ self.connect('key-press-event', self._key_pressed)
+
+ #~ self.icon_theme = Gtk.icon_theme_get_default()
+ #~ self.set_icon(self.icon_theme.load_icon("stock_internet", 16, 0))
+
+ #~ self.set_default_size(800, 600)
+
+ self.show_all()
+ self.content_tabs.new_tab(self.start_url, True)
+
+
+
+ #~ def quit(self, window):
+ #~ num_pages = self.content_tabs.get_n_pages()
+ #~ while num_pages != -1:
+ #~ scrolled_window = self.content_tabs.get_nth_page(num_pages)
+ #~ if scrolled_window:
+ #~ web_view = scrolled_window.get_child()
+ #~ web_view.destroy()
+ #~ num_pages = num_pages - 1
+ #~ self.destroy()
+ #~ Gtk.main_quit()
+
+
+ def get_content_tabs(self):
+ return self.content_tabs
+
+
+ def load_requested(self, entry, text):
+ if not text:
+ return
+ try:
+ text.index("://")
+ except:
+ if (' ' in text and '/' not in text) or ('.' not in text):
+ text = 'https://www.google.si/search?q=' + text
+ else:
+ text = "http://" + text
+ self.content_tabs.load(text)
+
+
+ def _update_title(self, notebook, web_view, title, url):
+ #~ if not title is None:
+ #~ self.set_title(_("%s") % title)
+ if not url is None:
+ self.toolbar.entry.set_text(url)
+ self.toolbar.back_button.set_sensitive(web_view.can_go_back())
+ self.toolbar.forward_button.set_sensitive(web_view.can_go_forward())
+
+ def _key_pressed(self, widget, event):
+ mapping = {
+ 'r': self.content_tabs.refresh,
+ 'w': self.content_tabs.close_current_tab,
+ 't': self.content_tabs.new_tab,
+ 'l': self.toolbar.entry.grab_focus
+ }
+ if event.state & Gdk.ModifierType.CONTROL_MASK and Gdk.keyval_name(event.keyval).lower() in mapping:
+ mapping[Gdk.keyval_name(event.keyval)]()
+ elif event.state & Gdk.ModifierType.MOD1_MASK:
+ if Gdk.keyval_name(event.keyval) == 'F4':
+ self.quit(self)
+
+def returntrue(*args):
+ return True
+
+#~ if __name__ == "__main__":
+ #~ webbrowser = WebBrowser()
+ #~ Gtk.main()
Index: basico/gui.py
===================================================================
--- basico/gui.py (nonexistent)
+++ basico/gui.py (revision 4)
@@ -0,0 +1,152 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: gui.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: GUI module
+
+import os
+import sys
+import subprocess
+
+from datetime import datetime
+
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository.GdkPixbuf import Pixbuf
+from gi.repository import Pango
+from gi.repository import GLib
+from gi.repository import GObject
+from gi.repository import Gio
+
+from .service import Service
+from .uiapp import UIApp
+
+
+class GUI(Service):
+ def initialize(self):
+ '''
+ Setup GUI Service
+ '''
+ # Invoke services
+ self.sap = self.app.get_service('SAP')
+
+ # Setup caches
+ self.widgets = {} # Widget name : Object
+ self.keys = {} # Key : Value; keys: doctype, property, values
+
+ # Setup Glade builder
+ self.setup_builder()
+
+
+ def run(self):
+ '''
+ Let GUI service start
+ '''
+ self.log.debug("Setting up GUI")
+ #~ Gdk.threads_init()
+
+ GObject.threads_init()
+ self.sap.run()
+ self.ui = UIApp(self.app)
+ self.ui.run()
+
+
+ def quit(self, *args):
+ '''
+ GUI destroyed
+ '''
+ self.app.stop()
+
+
+ def end(self):
+ '''
+ End GUI Service
+ '''
+ self.log.debug("GUI terminated")
+ self.ui.quit()
+
+
+ def setup_builder(self):
+ '''
+ Setup Gtk.Builder object
+ '''
+ DIR_UI = self.get_var('UI')
+ self.builder = Gtk.Builder()
+ self.builder.add_from_file(DIR_UI + 'basico.ui')
+
+
+ def get_builder(self):
+ '''
+ Return Gtk.Builder object
+ '''
+ return self.builder()
+
+
+ def swap_widget(self, parent, widget):
+ '''
+ Swap widget inside a container
+ '''
+ try:
+ child = parent.get_children()[0]
+ parent.remove(child)
+ parent.add(widget)
+ del (child)
+ except:
+ parent.add(widget)
+ widget.show_all()
+
+
+ def get_key(self, key):
+ '''
+ Return current value from var cache
+ '''
+ return self.keys[key]
+
+
+ def set_key(self, key, value):
+ '''
+ Set current value for var cache
+ '''
+ self.keys[key] = value
+
+
+ def add_widget(self, name, obj=None):
+ '''
+ Add widget to cache
+ '''
+ try:
+ if obj is not None:
+ self.widgets[name] = obj
+ else:
+ self.widgets[name] = self.builder.get_object(name)
+ return self.widgets[name]
+ except Exception as error:
+ self.log.error (self.get_traceback())
+ return None
+
+
+ def get_widget(self, name):
+ '''
+ Return widget from cache
+ '''
+ try:
+ return self.widgets[name]
+ except KeyError:
+ #~ self.log.error("Widget '%s' not found" % name)
+ #~ return None
+ return self.add_widget(name)
+
+
+ def get_widgets(self):
+ return self.widgets
+
+
+ def get_app(self):
+ return self.ui
+
+
+ def get_window(self):
+ return self.ui.get_window()
/basico/gui.py
Property changes:
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Index: basico/projects.py
===================================================================
--- basico/projects.py (nonexistent)
+++ basico/projects.py (revision 4)
@@ -0,0 +1,240 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: projects.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Projects service
+
+
+from gi.repository import Gtk
+from gi.repository.GdkPixbuf import Pixbuf
+from gi.repository import Pango
+from datetime import datetime
+from dateutil import parser as dateparser
+
+from .service import Service
+
+
+class Projects(Service):
+ def initialize(self):
+ self.get_services()
+ self.setup_window()
+
+
+ def setup_window(self):
+ # setup widgets
+ self.window = self.gui.add_widget('winProjects')
+ self.parent = self.gui.get_widget('mainwindow')
+ self.window.set_transient_for(self.parent)
+ self.pname = self.gui.add_widget('etyProjectName')
+ self.pname.connect('activate', self.add_project)
+ self.boxproj = self.gui.add_widget('boxProjects')
+ self.btnadd = self.gui.add_widget('btnAddProject')
+ self.btnadd.connect('clicked', self.add_project)
+ self.btndel = self.gui.add_widget('btnDelProject')
+ self.btndel.connect('clicked', self.delete_project)
+ self.btncancel = self.gui.add_widget('btnCancelProjects')
+ self.btncancel.connect('clicked', self.hide_window)
+ self.btnaccept = self.gui.add_widget('btnAcceptProjects')
+ self.btnaccept.connect('clicked', self.link_to_project)
+ self.treeview = self.gui.add_widget('trvprojwin', Gtk.TreeView())
+ self.boxproj.add(self.treeview)
+ self.hide_window()
+
+ # setup model
+ model = Gtk.ListStore(
+ bool, # CheckBox
+ str # Project name
+ )
+ self.treeview.set_model(model)
+
+ # setup columns
+ # Checkbox
+ renderer = Gtk.CellRendererToggle()
+ renderer.connect("toggled", self.on_cell_toggled)
+ column = Gtk.TreeViewColumn('X', renderer, active=0)
+ column.set_visible(True)
+ column.set_expand(False)
+ column.set_clickable(False)
+ column.set_sort_indicator(False)
+ self.treeview.append_column(column)
+
+ # Project name
+ renderer = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn('Project name', renderer, text=1)
+ column.set_visible(True)
+ column.set_expand(True)
+ column.set_clickable(True)
+ column.set_sort_indicator(True)
+ self.treeview.append_column(column)
+
+ # Treeview features
+ self.treeview.set_headers_visible(True)
+ self.treeview.set_enable_search(True)
+ self.treeview.set_grid_lines(Gtk.TreeViewGridLines.HORIZONTAL)
+ self.treeview.set_search_column(1)
+ self.treeview.connect('row-activated', self.double_click)
+ selection = self.treeview.get_selection()
+ selection.set_mode(Gtk.SelectionMode.SINGLE)
+ self.treeview.set_search_entry(self.pname)
+
+
+ def delete_project(self, *args):
+ found = 0
+ selection = self.treeview.get_selection()
+ result = selection.get_selected()
+ if result: #result could be None
+ model, iter = result
+ project = model.get_value(iter, 1)
+ self.log.debug(project)
+ sapnotes = self.sap.get_notes()
+ for sapnote in sapnotes:
+ projects = sapnotes[sapnote]['projects']
+ if project in projects:
+ found += 1
+ if found > 1:
+ self.log.debug("Project %s is still assigned to other SAP Notes" % project)
+ head = "Project could not be deleted"
+ body = "Project %s is still assigned to other SAP Notes" % project
+ self.uif.message_dialog(head , body)
+ else:
+ model.remove(iter)
+
+
+
+
+ def add_project(self, *args):
+ project = self.pname.get_text()
+ model = self.treeview.get_model()
+
+ found = False
+ for row in model:
+ if row[1].upper() == project.upper():
+ found = True
+
+ if not found and len(project) > 0:
+ model.append([False, project])
+
+ self.pname.set_text('')
+
+
+
+ def show_window(self, sapnote=None):
+ rootwin = self.gui.get_widget('mainwindow')
+ self.window.set_transient_for(rootwin)
+ self.load_projects(sapnote)
+ self.window.set_no_show_all(False)
+ self.window.show_all()
+
+ #~ if sapnote is not None:
+ #~ projects = self.sap.get_linked_projects(sapnote)
+ #~ self.log.debug("Projects for SAP Note %s: %s" % (sapnote, projects))
+ #~ dialog = Gtk.MessageDialog(self.parent, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "Task completed")
+ #~ dialog.format_secondary_text("All SAP Notes were downloaded sucessfully.")
+ #~ dialog.run()
+ #~ dialog.destroy()
+
+ def hide_window(self, *args):
+ self.window.set_no_show_all(True)
+ self.window.hide()
+
+
+ def get_services(self):
+ self.gui = self.app.get_service("GUI")
+ self.sap = self.app.get_service('SAP')
+ self.uif = self.app.get_service('UIF')
+
+
+ def double_click(self, treeview, row, col):
+ model = treeview.get_model()
+ self.pname.set_text(model[row][1])
+
+
+ def populate(self, sapnotes):
+ model = self.treeview.get_model()
+ model.clear()
+ #~ model.append([bool, str])
+
+
+ def changed(self, *args):
+ try:
+ model, treeiters = self.selection.get_selected_rows()
+ selected = set()
+ if len(treeiters) > 0:
+ for treeiter in treeiters:
+ if treeiter != None:
+ selected.add(model[treeiter][0])
+ print (selected)
+
+ except Exception as error:
+ self.log.error (self.get_traceback())
+
+
+ def on_cell_toggled(self, widget, path):
+ model = self.treeview.get_model()
+ model[path][0] = not model[path][0]
+
+
+ def get_selected_notes(self):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ return sapnoteview.get_selected_notes()
+
+
+ def link_to_project(self, *args):
+ self.save_projects()
+ self.hide_window()
+ sapnotes = list(self.get_selected_notes())
+ model = self.treeview.get_model()
+ projects = []
+ for row in model:
+ link = row[0]
+ if link == True:
+ projects.append(row[1])
+
+ sapnotes.sort()
+ projects.sort()
+ self.log.debug('SAP Notes: %s' % sapnotes)
+ self.log.debug(' Projects: %s' % projects)
+ self.sap.link_to_project(sapnotes, projects)
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnoteview.refresh()
+
+
+ def get_all_projects(self):
+ projects = []
+ model = self.treeview.get_model()
+ for row in model:
+ projects.append(row[1])
+ projects.sort()
+
+ return projects
+
+
+ def load_projects(self, sapnote=''):
+ try:
+ projects = self.get_config_value('Projects').split(',')
+ except:
+ projects = []
+ model = self.treeview.get_model()
+ model.clear()
+
+ for project in projects:
+ if len(project) > 0:
+ if len(sapnote) > 0:
+ snprjs = self.sap.get_linked_projects(sapnote)
+ self.log.debug(snprjs)
+ if project in snprjs:
+ model.append([True, project])
+ else:
+ model.append([False, project])
+ else:
+ model.append([False, project])
+
+
+ def save_projects(self):
+ settings = {}
+ projects = self.get_all_projects()
+
+ settings['Projects'] = ','.join(projects)
+ self.config[self.section] = settings
+ self.save_config()
Index: basico/basico.py
===================================================================
--- basico/basico.py (nonexistent)
+++ basico/basico.py (revision 4)
@@ -0,0 +1,215 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# File: basico.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Main entry point por Basico app
+import os
+import sys
+import traceback as tb
+import imp
+import signal
+from os.path import abspath, sep as SEP
+from configparser import SafeConfigParser, ExtendedInterpolation
+
+#~ from gi.repository import GObject
+
+from .log import get_logger
+from .gui import GUI
+from .iconmanager import IconManager
+from .sap import SAP
+from .settings import Settings
+from .uifuncs import UIFuncs
+from .menus import Menus
+from .projects import Projects
+from .tasks import Tasks
+from .plugins import Plugins
+from .callbacks import Callback
+from .notify import Notification
+from .stats import Stats
+from .env import ROOT, APP, LPATH, GPATH, FILE
+
+
+class Basico:
+ def __init__(self):
+ """Main class: the entry point for Basico.
+ It stands for Controller.
+ """
+ self.__set_env()
+ self.log = get_logger(self.__class__.__name__, FILE['LOG'])
+ self.log.info("Starting Basico")
+ self.__init_config()
+ self.services = {}
+ try:
+ services = {
+ 'GUI' : GUI(),
+ 'UIF' : UIFuncs(),
+ 'Menus' : Menus(),
+ 'SAP' : SAP(),
+ 'Settings' : Settings(),
+ 'Notify' : Notification(),
+ 'Tasks' : Tasks(),
+ 'IM' : IconManager(),
+ 'Plugins' : Plugins(),
+ 'Callbacks' : Callback(),
+ 'Stats' : Stats(),
+ }
+ self.register_services(services)
+ except Exception as error:
+ self.log.error(error)
+ raise
+
+
+ def __set_env(self):
+ # Create local paths if they do not exist
+ for DIR in LPATH:
+ if not os.path.exists(LPATH[DIR]):
+ os.makedirs(LPATH[DIR])
+
+
+ def __init_config(self):
+ #~ self.log.debug("ROOT: %s" % ROOT)
+ # Set up config
+ CONFIG_FILE = self.get_file('CNF')
+
+ #~ https://docs.python.org/3/library/configparser.html#interpolation-of-values
+ self.config = SafeConfigParser(interpolation=ExtendedInterpolation())
+
+ #~ https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.optionxform
+ self.config.optionxform = str
+
+ # Save config
+ if not os.path.exists(CONFIG_FILE):
+ self.log.debug('Configuration file not found. Creating a new one')
+ with open(CONFIG_FILE, 'w') as configfile:
+ self.config.write(configfile)
+ self.log.info('Config file initialited')
+
+
+ def get_config(self):
+ CONFIG_FILE = self.get_file('CNF')
+ self.config.read(CONFIG_FILE)
+
+ return self.config
+
+
+ def get_file(self, name):
+ try:
+ return FILE[name]
+ except:
+ self.log.error(self.get_traceback())
+
+
+ def get_app_info(self, var):
+ try:
+ return APP[var]
+ except:
+ return None
+
+
+ def get_var(self, name, scope='global'):
+ if scope == 'global':
+ return GPATH[name]
+ else:
+ return LPATH[name]
+
+
+ def list_services(self):
+ """Return a dictionary of services"""
+ return self.services
+
+
+ def get_service(self, name):
+ """Get/Start a registered service
+ @type name: name of the service
+ @param name: given a service name it returns the associated
+ class. If service was not running it is started.
+ """
+ try:
+ service = self.services[name]
+ if service.is_started():
+ return service
+ else:
+ service.start(self, name)
+ return service
+ except KeyError as service:
+ self.log.error("Service %s not registered or not found" % service)
+ raise
+
+ def register_services(self, services):
+ """Register a list of services
+ @type services: dict
+ @param services: a dictionary of name:class for each service
+ """
+ for name in services:
+ self.register_service(name, services[name])
+
+
+ def register_service(self, name, service):
+ """Register a new service
+ @type name: string
+ @param name: name of the service
+ @type service: class
+ @param service: class which contains the code
+ """
+ try:
+ self.services[name] = service
+ #~ self.log.debug("Service '%s' loaded successfully" % name)
+ except Exception as error:
+ self.log.error(error)
+
+
+ def deregister_service(self, name):
+ """Deregister a running service
+ @type name: string
+ @param name: name of the service
+ """
+ self.services[name].end()
+ self.services[name] = None
+
+
+ def check(self):
+ sap = self.get_service('SAP')
+ found = sap.check_webdriver()
+ if not found:
+ self.log.error("No webdriver found. Exiting.")
+ return found
+
+
+ def stop(self):
+ """For each service registered, it executes the 'end' method
+ (if any) to finalize them properly.
+ """
+
+ # Deregister all services loaded
+ self.deregister_service('GUI')
+
+ for name in self.services:
+ try:
+ self.deregister_service(name)
+ except: pass
+ # Bye bye
+ self.log.info("Basico finished")
+
+
+ def get_traceback(self):
+ return tb.format_exc()
+
+
+ def run(self):
+ try:
+ self.gui = self.get_service('GUI')
+ #~ self.log.debug("Basico ready to start")
+ self.gui.run()
+ except:
+ self.log.error(self.get_traceback())
+
+
+def main():
+ #DOC: http://stackoverflow.com/questions/16410852/keyboard-interrupt-with-with-python-gtk
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+ basico = Basico()
+ run = True #basico.check()
+ if run:
+ basico.run()
+ sys.exit(0)
Index: basico/window.py
===================================================================
--- basico/window.py (nonexistent)
+++ basico/window.py (revision 4)
@@ -0,0 +1,393 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: window.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Gtk.ApplicationWindow implementation
+
+import os
+import stat
+import threading
+import time
+import platform
+
+import gi
+gi.require_version('Gtk', '3.0')
+gi.require_version('WebKit', '3.0')
+
+from gi.repository import GLib
+from gi.repository import GObject
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import Gio
+from gi.repository import Pango
+from gi.repository import WebKit
+from gi.repository.GdkPixbuf import Pixbuf
+
+from .log import get_logger
+from .sapnoteview import SAPNoteView
+
+#~ from .browser import WebBrowser
+
+MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
+
+class GtkAppWindow(Gtk.ApplicationWindow):
+ def __init__(self, uiapp):
+ self.setup_controller(uiapp)
+ self.get_services()
+ self.gui.add_widget('uiapp', uiapp)
+ self.gui.set_key('cmbvalue', 'search')
+ self.settings = {}
+ self.settings['fullscreen'] = False
+ self.current_notes = {}
+
+ self.setup_window()
+ self.setup_widgets()
+ self.setup_app()
+ self.check_settings()
+
+
+ def check_settings(self):
+ # SAP module settings
+ settings = self.sap.get_default_settings()
+ for key in settings:
+ try:
+ self.sap.get_config_value(key)
+ except:
+ self.sap.set_config_value(key, settings[key])
+ #~ self.log.warning("Setting '%s' does not exists" % setting)
+
+
+ def setup_controller(self, uiapp):
+ self.uiapp = uiapp
+ self.controller = uiapp.get_controller()
+
+
+ def setup_app(self):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ searchentry = self.gui.get_widget("stySearchInfo")
+ viewlabel = self.gui.get_widget('lblViewCurrent')
+ try:
+ name = sapnoteview.get_view()
+ except:
+ name = 'components'
+ sapnoteview.set_view(name)
+ label = "<span size='20000'><b>%-10s</b></span>" % name.capitalize()
+ viewlabel.set_markup(label)
+ self.cb.refresh_view()
+ searchentry.set_text('')
+ self.cb.search_notes()
+ sapnoteview.check_states()
+
+
+ def setup_window(self):
+ app_title = self.controller.get_app_info('name')
+ Gtk.Window.__init__(self, title=app_title, application=self.uiapp)
+ self.gui.add_widget('mainwindow', self)
+ icon = self.im.get_icon('basico')
+ self.set_icon(icon)
+ # FIXME
+ # From docs: Don’t use this function. It sets the X xlib.Window
+ # System “class” and “name” hints for a window.
+ # But I have to do it or it doesn't shows the right title. ???
+ self.set_wmclass (app_title, app_title)
+ self.set_role(app_title)
+ #~ self.set_icon_from_file(self.env.get_var('ICONS') + '/basico.png')
+ self.set_default_size(1024, 728)
+ self.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
+ #~ self.connect('destroy', Gtk.main_quit)
+ self.show_all()
+
+
+ def setup_menu_views(self):
+ # View label
+ self.gui.add_widget('lblViewCurrent')
+
+ ## Views Menu
+ views_menu = self.gui.add_widget('mnuviews', Gio.Menu())
+
+ # Last added view
+ #~ TODO
+ #~ views_menu.append_item(self.uif.create_item('View last added', 'app.view-lastadded', ''))
+ #~ self.app.add_action(self.uif.create_action("view-lastadded", self.cb_show_dlnotes_window))
+
+ # Most used view
+ #~ TODO
+ #~ views_menu.append_item(self.uif.create_item('View most used', 'app.view-mostused', ''))
+ #~ self.app.add_action(self.uif.create_action("view-mostused", self.cb_show_dlnotes_window))
+
+ # Tasks view
+ views_menu.append_item(self.uif.create_item('View by tasks', 'app.view-tasks', 'emblem-system'))
+ self.app.add_action(self.uif.create_action("view-tasks", self.cb.refresh_view, 'tasks'))
+
+ # Projects view
+ #~ views_menu.append_item(self.uif.create_item('View by projects', 'app.view-projects', ''))
+ #~ self.app.add_action(self.uif.create_action("view-projects", self.cb.refresh_view, 'projects'))
+
+ # Components view
+ views_menu.append_item(self.uif.create_item('View by components', 'app.view-components', ''))
+ self.app.add_action(self.uif.create_action("view-components", self.cb.refresh_view, 'components'))
+
+ # Bookmarks view
+ views_menu.append_item(self.uif.create_item('View bookmarks', 'app.view-bookmarks', ''))
+ self.app.add_action(self.uif.create_action("view-bookmarks", self.cb.refresh_view, 'bookmarks'))
+
+
+ # Annotations view
+ #~ TODO
+ #~ views_menu.append_item(self.uif.create_item('View by annotations', 'app.view-annotations', ''))
+ #~ self.app.add_action(self.uif.create_action("view-annotations", self.cb_show_dlnotes_window))
+
+ # Set menu model in button
+ btnviews = self.gui.get_widget('mnuBtnViews')
+ btnviews.set_menu_model(views_menu)
+
+
+ def setup_menu_settings(self):
+ ### SETTINGS POPOVER
+ menu = Gio.Menu()
+ #~ self.gui.add_widget("menu", menu)
+ menu.append_item(self.uif.create_item('Fullscreen', 'app.settings-fullscreen', 'gtk-fullscreen'))
+ menu.append_item(self.uif.create_item('Rename current project', 'app.project-rename', 'preferences-desktop-personal'))
+ menu.append_item(self.uif.create_item('Refresh current project', 'app.project-refresh', 'view-refresh'))
+ menu.append_item(self.uif.create_item('Close current project', 'app.project-close', 'window-close'))
+ menu.append_item(self.uif.create_item('Delete current project', 'app.project-delete', 'list-remove'))
+ menu.append_item(self.uif.create_item('Export current project', 'app.project-delete', 'document-save-as'))
+ window = self.gui.get_window()
+ window.set_app_menu(menu)
+ app = self.gui.get_app()
+ app.add_action(self.uif.create_item("settings-fullscreen"))
+
+ #~ popover_action_group = Gio.SimpleActionGroup()
+ btnsettings = self.gui.get_widget("mnuBtnViews")
+ popover_settings = Gtk.Popover.new_from_model(btnsettings, menu)
+ popover_settings.set_position(Gtk.PositionType.BOTTOM)
+ btnsettings.connect('clicked', lambda _: popover_settings.show_all())
+
+
+ def setup_menus(self):
+ self.setup_menu_views()
+ #~ self.setup_menu_actions()
+ #~ self.setup_menu_settings()
+
+ def show_home_page(self, *args):
+ notebook = self.gui.get_widget('notebook')
+ notebook.set_current_page(0)
+
+ def show_settings_page(self, *args):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnoteview.set_view('settings')
+ notebook = self.gui.get_widget('notebook')
+ notebook.set_current_page(2)
+
+ def show_browser_page(self, *args):
+ notebook = self.gui.get_widget('notebook')
+ notebook.set_current_page(1)
+
+
+ def show_search(self, *args):
+ revsearch = self.gui.get_widget('revSearch')
+ tgbsearch = self.gui.get_widget('tgbSearch')
+ if tgbsearch.get_active():
+ revsearch.set_reveal_child(True)
+ revsearch.set_no_show_all(False)
+ revsearch.show_all()
+ else:
+ revsearch.set_reveal_child(False)
+ revsearch.set_no_show_all(True)
+ revsearch.hide()
+
+
+ def show_addsapnotes_dialog(self, *args):
+ sapnoteview = self.gui.get_widget('sapnoteview')
+ sapnoteview.set_view('download')
+ notebook = self.gui.get_widget('notebook')
+ notebook.set_current_page(1)
+
+
+ def setup_widgets(self):
+ self.mainbox = self.gui.get_widget('mainbox')
+ self.mainbox.reparent(self)
+
+ self.setup_menus()
+
+ notesbox = self.gui.get_widget('notesbox')
+ sapnoteview = SAPNoteView(self.controller)
+ self.gui.add_widget('sapnoteview', sapnoteview)
+ #~ self.log.debug("SAPNoteView widget added")
+ self.gui.add_widget('combobox')
+ search = self.gui.add_widget('stySearchInfo')
+ #~ search.connect('search_changed', self.search_notes)
+ search.connect('activate', self.cb.search_notes)
+ self.setup_combobox()
+ self.swap_widget(notesbox, sapnoteview)
+
+ # Buttons
+ btnabout = self.gui.get_widget('btnAbout')
+ btnabout.connect('clicked', self.uiapp.cb_show_about)
+
+ btnsettings = self.gui.get_widget('btnSettings')
+ btnsettings.connect('clicked', self.show_settings_page)
+
+ btnaddnote = self.gui.get_widget('btnStartDlNotes')
+ btnaddnote.connect('clicked', self.cb.import_notes)
+
+ refreshview = self.gui.get_widget('btnRefreshSAPNoteView')
+ refreshview.connect('clicked', self.cb.refresh_and_clear_view)
+
+ btnShowAddSAPNotesDlg = self.gui.get_widget('btnShowAddSAPNotesDlg')
+ btnShowAddSAPNotesDlg.connect('clicked', self.show_addsapnotes_dialog)
+
+ btnStopDlNotes = self.gui.get_widget('btnStopDlNotes')
+ btnStopDlNotes.connect('clicked', self.cb.stop_dl_notes)
+
+ tgbsearch = self.gui.get_widget('tgbSearch')
+ tgbsearch.connect('toggled', self.show_search)
+ revsearch = self.gui.get_widget('revSearch')
+ revsearch.hide()
+ revsearch.set_no_show_all(True)
+
+ toggle= self.gui.get_widget('tgbFullScreen')
+ toggle.connect('toggled', self.uif.fullscreen)
+ switch = self.gui.get_widget('schExpandCollapse')
+ switch.connect('state-set', sapnoteview.expand_collapse)
+ switch = self.gui.get_widget('schSelectNotesAllNone')
+ switch.connect('state-set', sapnoteview.select_all_none)
+
+ button = self.gui.get_widget('btnQuit')
+ button.connect('clicked', self.gui.quit)
+
+ # Actions button
+ button = self.gui.get_widget('mnuBtnActions')
+
+ # Prefs for SAP module
+ button = self.gui.add_widget('btnPrefsSAPApply')
+ button.connect('clicked', self.cb.apply_preferences)
+
+ button = self.gui.add_widget('btnPrefsSAPCancel')
+ button.connect('clicked', self.cb.refresh_view)
+
+ button = self.gui.add_widget('btnPrefsSAPReset')
+ button.connect('clicked', self.sap.default_preferences)
+
+ # Notebook Import Widget
+ ntbimport = self.gui.add_widget('ntbAddSAPNotes')
+
+ try:
+ sap_settings = self.sap.get_custom_settings()
+ except:
+ self.log.error(self.controller.get_traceback())
+ sap_settings = self.sap.get_default_settings()
+ self.log.debug("SAP Default settings loaded")
+
+ for setting in sap_settings:
+ widget = self.gui.add_widget(setting)
+ widget.set_text(sap_settings[setting])
+
+ # Stats
+ statsviewer = self.gui.add_widget('scrStatsViewer')
+
+ btnstats = self.gui.add_widget('btnStatsByCompMain')
+ btnstats.connect('clicked', self.cb.update_components_stats)
+ iconwdg = self.gui.add_widget('imgStatsByCompMain')
+ icon = self.im.get_pixbuf_icon('component', 64, 64)
+ iconwdg.set_from_pixbuf(icon)
+
+ btnstats = self.gui.add_widget('btnStatsByCategory')
+ btnstats.connect('clicked', self.cb.update_categories_stats)
+ iconwdg = self.gui.add_widget('imgStatsByCategory')
+ icon = self.im.get_pixbuf_icon('category', 64, 64)
+ iconwdg.set_from_pixbuf(icon)
+
+ self.show_all()
+ self.log.debug("GUI loaded")
+
+
+ def __completion_func(self, completion, key, iter):
+ model = completion.get_model()
+ text = model.get_value(iter, 0)
+ if key.upper() in text.upper():
+ return True
+ return False
+
+
+ def setup_combobox_completions(self, key):
+ model = Gtk.ListStore(str)
+ search = self.gui.get_widget("stySearchInfo")
+ completion = Gtk.EntryCompletion()
+ completion.set_model(model)
+ completion.set_text_column(0)
+ completion.set_match_func(self.__completion_func)
+ search.set_completion(completion)
+ #~ self.log.debug("Completion Key: %s" % key)
+
+ stats = self.sap.get_stats()
+
+ try:
+ items = list(stats[key])
+ items.sort()
+ for item in items:
+ model.append([item])
+ except:
+ pass
+
+
+ def cb_combobox_changed(self, combobox):
+ model = combobox.get_model()
+ treeiter = combobox.get_active_iter()
+ key = model[treeiter][0]
+ self.gui.set_key('cmbvalue', key)
+ self.setup_combobox_completions(key)
+
+
+ def setup_combobox(self):
+ combobox = self.gui.get_widget('combobox')
+ model = Gtk.TreeStore(str, str)
+ combobox.set_model(model)
+
+ ## key
+ cell = Gtk.CellRendererText()
+ cell.set_visible(False)
+ combobox.pack_start(cell, True)
+ combobox.add_attribute(cell, 'text', 0)
+
+ ## value
+ cell = Gtk.CellRendererText()
+ cell.set_alignment(0.0, 0.5)
+ combobox.pack_start(cell, expand=False)
+ combobox.add_attribute(cell, 'markup', 1)
+ combobox.connect('changed', self.cb_combobox_changed)
+
+ model.append(None, ['search', 'Search in all database'])
+ model.append(None, ['project', 'Filter by project name'])
+ model.append(None, ['task', 'Filter by task name'])
+ model.append(None, ['component', 'Filter by component'])
+ model.append(None, ['category', 'Filter by category'])
+ model.append(None, ['type', 'Filter by type'])
+ model.append(None, ['id', 'Filter by Id'])
+ model.append(None, ['title', 'Filter by title'])
+ model.append(None, ['priority', 'Filter by priority'])
+ model.append(None, ['version', 'Filter by version'])
+ #~ model.append(None, ['released', 'Filter by release date'])
+
+ combobox.set_active(0)
+
+
+ def get_services(self):
+ """Load services to be used in this class
+ """
+ self.gui = self.controller.get_service("GUI")
+ self.app = self.gui.get_app()
+ LOG_FILE = self.controller.get_file('LOG')
+ self.log = get_logger('GtkAppWindow', LOG_FILE)
+ self.sap = self.controller.get_service("SAP")
+ self.uif = self.controller.get_service("UIF")
+ self.prefs = self.controller.get_service("Settings")
+ self.im = self.controller.get_service('IM')
+ self.cb = self.controller.get_service('Callbacks')
+
+
+ def swap_widget(self, container, combobox):
+ """Shortcut to GUI.swap_widget method
+ """
+ self.gui.swap_widget(container, combobox)
Index: LICENSE
===================================================================
--- LICENSE (nonexistent)
+++ LICENSE (revision 4)
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
Index: basico/data/icons/export-txt.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/export-txt.png
===================================================================
--- basico/data/icons/export-txt.png (nonexistent)
+++ basico/data/icons/export-txt.png (revision 4)
/basico/data/icons/export-txt.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/refresh.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/refresh.png
===================================================================
--- basico/data/icons/refresh.png (nonexistent)
+++ basico/data/icons/refresh.png (revision 4)
/basico/data/icons/refresh.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/plugin.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/plugin.png
===================================================================
--- basico/data/icons/plugin.png (nonexistent)
+++ basico/data/icons/plugin.png (revision 4)
/basico/data/icons/plugin.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/notask.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/notask.png
===================================================================
--- basico/data/icons/notask.png (nonexistent)
+++ basico/data/icons/notask.png (revision 4)
/basico/data/icons/notask.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/basico.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/basico.png
===================================================================
--- basico/data/icons/basico.png (nonexistent)
+++ basico/data/icons/basico.png (revision 4)
/basico/data/icons/basico.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/browse.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/browse.png
===================================================================
--- basico/data/icons/browse.png (nonexistent)
+++ basico/data/icons/browse.png (revision 4)
/basico/data/icons/browse.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/delete.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/delete.png
===================================================================
--- basico/data/icons/delete.png (nonexistent)
+++ basico/data/icons/delete.png (revision 4)
/basico/data/icons/delete.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/component.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/component.png
===================================================================
--- basico/data/icons/component.png (nonexistent)
+++ basico/data/icons/component.png (revision 4)
/basico/data/icons/component.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/idea.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/idea.png
===================================================================
--- basico/data/icons/idea.png (nonexistent)
+++ basico/data/icons/idea.png (revision 4)
/basico/data/icons/idea.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/noproject.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/noproject.png
===================================================================
--- basico/data/icons/noproject.png (nonexistent)
+++ basico/data/icons/noproject.png (revision 4)
/basico/data/icons/noproject.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/bookmark.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/bookmark.png
===================================================================
--- basico/data/icons/bookmark.png (nonexistent)
+++ basico/data/icons/bookmark.png (revision 4)
/basico/data/icons/bookmark.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/download.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/download.png
===================================================================
--- basico/data/icons/download.png (nonexistent)
+++ basico/data/icons/download.png (revision 4)
/basico/data/icons/download.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/sapnote.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/sapnote.png
===================================================================
--- basico/data/icons/sapnote.png (nonexistent)
+++ basico/data/icons/sapnote.png (revision 4)
/basico/data/icons/sapnote.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/category.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/category.png
===================================================================
--- basico/data/icons/category.png (nonexistent)
+++ basico/data/icons/category.png (revision 4)
/basico/data/icons/category.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/icons/tasks.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: basico/data/icons/tasks.png
===================================================================
--- basico/data/icons/tasks.png (nonexistent)
+++ basico/data/icons/tasks.png (revision 4)
/basico/data/icons/tasks.png
Property changes:
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: basico/data/ui/basico.ui
===================================================================
--- basico/data/ui/basico.ui (nonexistent)
+++ basico/data/ui/basico.ui (revision 4)
@@ -0,0 +1,2030 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3
+
+Copyright (C)
+
+This file is part of basico.ui.
+
+basico.ui 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 3 of the License, or
+(at your option) any later version.
+
+basico.ui 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 basico.ui. If not, see <http://www.gnu.org/licenses/>.
+
+Author: Tomás Vírseda <tomasvirseda@gmail.com>
+
+-->
+<interface domain="basico">
+ <requires lib="gtk+" version="3.12"/>
+ <!-- interface-license-type gplv3 -->
+ <!-- interface-name basico.ui -->
+ <!-- interface-description Basico UI XML file -->
+ <!-- interface-authors Tom\303\241s V\303\255rseda <tomasvirseda@gmail.com> -->
+ <object class="GtkAction" id="action1"/>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="upper">100</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment2">
+ <property name="upper">1800</property>
+ <property name="value">5</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkWindow" id="window">
+ <property name="can_focus">False</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="urgency_hint">True</property>
+ <child>
+ <object class="GtkBox" id="box1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox" id="mainbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="box2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">6</property>
+ <property name="margin_right">6</property>
+ <property name="margin_top">3</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="boxMenu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="vexpand">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">3</property>
+ <child>
+ <object class="GtkBox" id="box45">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkMenuButton" id="mnuBtnViews">
+ <property name="width_request">250</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkBox" id="box29">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkImage" id="imgViewCurrent">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-select-color</property>
+ <property name="icon_size">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="lblViewCurrent">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;big&gt;&lt;b&gt;View&lt;/b&gt;&lt;/big&gt;</property>
+ <property name="use_markup">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator" id="separator6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="mnuBtnActions">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <property name="use_popover">False</property>
+ <child>
+ <object class="GtkBox" id="box28">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">3</property>
+ <child>
+ <object class="GtkLabel" id="lblSelectedNotes">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;big&gt;0 SAP Notes selected&lt;/big&gt;</property>
+ <property name="use_markup">True</property>
+ <property name="justify">center</property>
+ <property name="yalign">0.69999998807907104</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator" id="separator8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkButton" id="btnRefreshSAPNoteView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkBox" id="box21">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkImage" id="image6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-refresh</property>
+ <property name="icon_size">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box51">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkButton" id="btnShowAddSAPNotesDlg">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkBox" id="box50">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkImage" id="image10">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-add</property>
+ <property name="icon_size">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box54">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkToggleButton" id="tgbSearch">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkImage" id="image13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-find</property>
+ <property name="icon_size">5</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator" id="separator9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box48">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkBox" id="box52">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="label10">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Selection</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="schSelectNotesAllNone">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box53">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="label11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Expand/Collapse</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="schExpandCollapse">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box12">
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator" id="separator7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">9</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">10</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox" id="box47">
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkToggleButton" id="tgbFullScreen">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkImage" id="image3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-fullscreen</property>
+ <property name="icon_size">5</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">13</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box46">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkButton" id="btnSettings">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage" id="image5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-preferences</property>
+ <property name="icon_size">5</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">14</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box19">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkButton" id="btnAbout">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage" id="image4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-about</property>
+ <property name="icon_size">5</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">15</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box27">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkButton" id="btnQuit">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage" id="image11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-quit</property>
+ <property name="icon_size">5</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">16</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="revSearch">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="transition_type">crossfade</property>
+ <child>
+ <object class="GtkBox" id="box49">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">3</property>
+ <child>
+ <object class="GtkComboBox" id="combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="focus_on_click">False</property>
+ <property name="popup_fixed_width">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSearchEntry" id="stySearchInfo">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="activates_default">True</property>
+ <property name="truncate_multiline">True</property>
+ <property name="primary_icon_name">edit-find-symbolic</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkNotebook" id="notebook">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="show_tabs">False</property>
+ <property name="show_border">False</property>
+ <child>
+ <object class="GtkBox" id="box6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="box3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="box7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="box9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkScrolledWindow" id="notesbox">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">out</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">SAP Notes Manager</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="boxOperations">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="box8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="box17">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkNotebook" id="ntbAddSAPNotes">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkBox" id="box56">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="box59">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">6</property>
+ <property name="margin_right">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="box11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">3</property>
+ <child>
+ <object class="GtkLabel" id="label22">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;big&gt;&lt;i&gt;Write down some SAP Notes ID (comma-separated or one per line)&lt;/i&gt;&lt;/big&gt; </property>
+ <property name="use_markup">True</property>
+ <property name="lines">0</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow4">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkTextView" id="txtSAPNotes">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;big&gt;&lt;b&gt;Download from SAPNet&lt;/b&gt;&lt;/big&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box10">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">6</property>
+ <property name="margin_right">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="box15">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="box57">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">6</property>
+ <property name="margin_right">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">3</property>
+ <child>
+ <object class="GtkLabel" id="label21">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;big&gt;&lt;i&gt;Select a BCO (Basico Package Object) file to import.&lt;/i&gt;&lt;/big&gt; </property>
+ <property name="use_markup">True</property>
+ <property name="lines">0</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box60">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">6</property>
+ <property name="margin_right">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkViewport" id="viewport2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkFileChooserWidget" id="fcwImportNotes">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="create_folders">False</property>
+ <property name="preview_widget_active">False</property>
+ <property name="use_preview_label">False</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;big&gt;&lt;b&gt;Import a BCO file&lt;/b&gt;&lt;/big&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButtonBox" id="buttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <property name="layout_style">spread</property>
+ <child>
+ <object class="GtkButton" id="btnStopDlNotes">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkBox" id="box14">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="stock">gtk-cancel</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Cancel</property>
+ <property name="use_markup">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </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="btnStartDlNotes">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkBox" id="box13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkImage" id="image2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="stock">gtk-apply</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label23">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Accept</property>
+ <property name="use_markup">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">SAP Notes Downloader</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box16">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkViewport" id="viewport1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkBox" id="box18">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkExpander" id="expander1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="expanded">True</property>
+ <child>
+ <object class="GtkAlignment" id="alignment11">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="top_padding">6</property>
+ <property name="bottom_padding">6</property>
+ <property name="left_padding">6</property>
+ <property name="right_padding">6</property>
+ <child>
+ <object class="GtkGrid" id="grid1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">6</property>
+ <property name="row_homogeneous">True</property>
+ <child>
+ <object class="GtkEntry" id="CNF_SAP_Login">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box20">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkEntry" id="CNF_SAP_SUser">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="max_length">12</property>
+ <property name="width_chars">12</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box22">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkEntry" id="CNF_SAP_SPass">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="max_length">256</property>
+ <property name="visibility">False</property>
+ <property name="width_chars">15</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="CNF_SAP_Logout">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="CNF_SAP_OData_Notes">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="CNF_SAP_Notes_URL">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label17">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">SAP Note URL</property>
+ <property name="xalign">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label16">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">OData SAP Notes Service</property>
+ <property name="xalign">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label15">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Logout page</property>
+ <property name="xalign">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label14">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Login page</property>
+ <property name="xalign">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">S-User password</property>
+ <property name="xalign">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">S-User ID</property>
+ <property name="xalign">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButtonBox" id="buttonbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="btnPrefsSAPReset">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkBox" id="box23">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkImage" id="image7">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-revert-to-saved</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label18">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Reset to default</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ <property name="secondary">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btnPrefsSAPCancel">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkBox" id="box44">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkImage" id="image9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-cancel</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Cancel changes</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btnPrefsSAPApply">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkBox" id="box24">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkImage" id="image8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-apply</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label19">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Apply changes</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label20">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Connection Timeout</property>
+ <property name="xalign">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="CNF_SAP_CONN_TIMEOUT">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="halign">start</property>
+ <property name="hexpand">False</property>
+ <property name="max_length">4</property>
+ <property name="width_chars">4</property>
+ <property name="input_purpose">digits</property>
+ <property name="adjustment">adjustment2</property>
+ <property name="climb_rate">1</property>
+ <property name="snap_to_ticks">True</property>
+ <property name="numeric">True</property>
+ <property name="value">5</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">6</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkBox" id="box25">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes"> &lt;b&gt;SAP connection preferences&lt;/b&gt; </property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Preferences</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box55">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="box26">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">3</property>
+ <child>
+ <object class="GtkBox" id="box31">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">6</property>
+ <property name="margin_right">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkButtonBox" id="buttonbox3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <property name="homogeneous">True</property>
+ <property name="layout_style">start</property>
+ <child>
+ <object class="GtkButton" id="btnStatsByCompMain">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkImage" id="imgStatsByCompMain">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-missing-image</property>
+ <property name="icon_size">6</property>
+ </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="btnStatsByCategory">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkImage" id="imgStatsByCategory">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-missing-image</property>
+ <property name="icon_size">6</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btnStatsByCompMain2">
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="no_show_all">True</property>
+ <property name="relief">none</property>
+ <property name="focus_on_click">False</property>
+ <child>
+ <object class="GtkImage" id="imgStatsByCompMain2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-missing-image</property>
+ <property name="icon_size">6</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrStatsViewer">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">never</property>
+ <property name="overlay_scrolling">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Stats</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="position">3</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkEntryBuffer" id="entrybuffer1"/>
+ <object class="GtkFileFilter" id="filefilter1"/>
+ <object class="GtkListStore" id="liststore1"/>
+ <object class="GtkTextBuffer" id="textbuffer1"/>
+ <object class="GtkWindow" id="winTasks">
+ <property name="width_request">400</property>
+ <property name="height_request">320</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Manage tasks</property>
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="window_position">center-always</property>
+ <property name="icon_name">applications-utilities</property>
+ <property name="deletable">False</property>
+ <child>
+ <object class="GtkBox" id="box35">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">3</property>
+ <property name="margin_right">3</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="box36">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">3</property>
+ <property name="margin_bottom">3</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">3</property>
+ <child>
+ <object class="GtkBox" id="box37">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkEntry" id="etyTaskName">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box38">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkButton" id="btnAddTask">
+ <property name="label">gtk-add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="use_stock">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="always_show_image">True</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="btnDelTask">
+ <property name="label">gtk-delete</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="use_stock">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="always_show_image">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box39">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">3</property>
+ <property name="margin_bottom">3</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="box41">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="box42">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkViewport" id="boxTasks">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator" id="separator5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box43">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">3</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkButtonBox" id="buttonbox4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkButton" id="btnCancelTasks">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="use_stock">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="always_show_image">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btnAcceptTasks">
+ <property name="label">gtk-ok</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">none</property>
+ <property name="use_stock">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="always_show_image">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="window1">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox" id="box30">
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkFrame" id="frame1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkBox" id="box58">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkEntry" id="etyFormatExportTXT">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="label24">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">&lt;b&gt; Format &lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
Index: basico/sap.py
===================================================================
--- basico/sap.py (nonexistent)
+++ basico/sap.py (revision 4)
@@ -0,0 +1,534 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: sap.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: SAP service
+
+import sys
+import time
+import json
+import traceback
+from shutil import which
+from cgi import escape
+import selenium
+from selenium import webdriver
+from selenium.webdriver.support.wait import WebDriverWait
+from selenium import webdriver
+from selenium.webdriver.common.keys import Keys
+#~ from selenium.webdriver.support.ui import WebDriverWait
+from selenium.common.exceptions import WebDriverException
+from selenium.common.exceptions import NoSuchElementException
+import feedparser
+
+
+from .service import Service
+
+# Default settings for SAP module
+LOGIN_PAGE_URL = "https://accounts.sap.com"
+LOGOUT_PAGE_URL = "https://accounts.sap.com/ui/logout"
+ODATA_NOTE_URL = "https://launchpad.support.sap.com/services/odata/svt/snogwscorr/TrunkSet(SapNotesNumber='%s',Version='0',Language='E')" #$expand=LongText" #?$expand=LongText,RefTo,RefBy"
+SAP_NOTE_URL = "https://launchpad.support.sap.com/#/notes/%s"
+TIMEOUT = 5
+
+
+class SAP(Service):
+ def initialize(self):
+ '''
+ Setup AppLogic Service
+ '''
+ self.windriver = None
+ self.sapnotes = {}
+ self.stats = {}
+ self.stats['maincomp'] = {}
+ self.stats['cats'] = {}
+ self.stats['component'] = set()
+ self.stats['category'] = set()
+ self.stats['priority'] = set()
+ self.stats['type'] = set()
+ self.stats['version'] = set()
+ #~ self.stats['releaseon'] = set()
+ self.__init_config_section()
+
+
+ def __init_config_section(self):
+ self.config = self.app.get_config()
+ if self.config.has_section(self.section):
+ options = self.config.options(self.section)
+ if len(options) == 0:
+ self.log.debug("Section %s empty. Initializing with default values" % self.section)
+ settings = self.get_default_settings()
+ self.config[self.section] = settings
+ self.save_config()
+
+
+ def ajax_complete(self, driver):
+ try:
+ return 0 == driver.execute_script("return jQuery.active")
+ except WebDriverException:
+ pass
+
+
+ def wait_for_page_load(self, driver):
+ driver.implicitly_wait(0.5)
+ try:
+ WebDriverWait(driver, 30).until(lambda d: (d.execute_script('return document.readyState') == 'complete'))
+ #~ WebDriverWait(driver, 30).until(lambda d: (d.execute_script('return window.performance.timing.loadEventEnd') > 0))
+ except Exception as error:
+ #~ self.log.warning(self.get_traceback())
+ self.log.warning(error)
+ time.sleep(5)
+
+
+ def check_webdriver(self):
+ found = False
+ driver = self.get_webdriver()
+ if driver:
+ driver.quit()
+ found = True
+
+ return found
+
+
+ def build_stats(self, bag=None):
+ if bag is None:
+ bag = self.sapnotes
+ self.dstats = {}
+ self.compstats = {}
+ alltasks = set()
+
+ for sid in bag:
+ # tasks
+ try:
+ tasks = self.sapnotes[sid]['tasks']
+ for task in tasks:
+ alltasks.add(task)
+ except: pass
+ self.tasks = self.app.get_service('Tasks')
+ self.tasks.save_tasks_from_stats(alltasks)
+
+ # components
+ compkey = self.sapnotes[sid]['componentkey']
+ comptxt = self.sapnotes[sid]['componenttxt']
+ component = escape("%s (%s)" % (compkey, comptxt))
+ sep = compkey.find('-')
+ if sep > 0:
+ maincomp = compkey[0:sep]
+ else:
+ maincomp = compkey
+
+ # categories
+ category = category = escape(self.sapnotes[sid]['category'])
+ try:
+ cont = self.stats['cats'][category]
+ self.stats['cats'][category] = cont + 1
+ except:
+ self.stats['cats'][category] = 1
+
+ # Build al (sub)keys from given component key
+ # useful for display stats with pygal
+ compkeys = compkey.split('-')
+ total = len(compkeys)
+ key = ''
+ i = 0
+ for subkey in compkeys:
+ key = key + '-' + subkey
+ if key[:1] == '-':
+ key = key[1:]
+
+ # update stats
+ try:
+ count = self.compstats[key]
+ self.compstats[key] = count + 1
+ except Exception as error:
+ self.compstats[key] = 1
+
+ try:
+ cont = self.stats['maincomp'][maincomp]
+ self.stats['maincomp'][maincomp] = cont + 1
+ except:
+ self.stats['maincomp'][maincomp] = 1
+
+ category = escape(self.sapnotes[sid]['category'])
+ priority = escape(self.sapnotes[sid]['priority'])
+ ntype = escape(self.sapnotes[sid]['type'])
+ version = escape(self.sapnotes[sid]['version'])
+ releaseon = escape(self.sapnotes[sid]['releaseon'])
+ self.stats['component'].add(component)
+ self.stats['category'].add(category)
+ self.stats['priority'].add(priority)
+ self.stats['type'].add(ntype)
+ self.stats['version'].add(version)
+ #~ self.stats['releaseon'].add(releaseon)
+ #~ self.stats[''].add(version)
+ #~ self.log.debug(self.compstats)
+ #~ self.log.debug("==")
+ #~ self.log.debug(self.stats)
+ self.log.debug(self.stats['maincomp'])
+
+
+
+ def get_stats(self):
+ return self.stats
+
+
+ def download_note(self, sapnote, driver):
+ downloaded = False
+ try:
+ self.log.debug("Downloading SAP Note %s" % sapnote)
+ self.log.debug("Waiting for page %s..." % (ODATA_NOTE_URL % sapnote))
+ driver.get(ODATA_NOTE_URL % sapnote)
+ #wait for ajax items to load
+ #~ WebDriverWait(driver, 60).until(self.ajax_complete, "Timeout waiting for page to load")
+ time.sleep(5)
+ self.log.debug("Page loaded")
+ downloaded = self.add_note(sapnote, driver.page_source)
+ except:
+ self.log.error("SAP Note %s coud not be downloaded" % sapnote)
+
+ return downloaded
+
+
+ def connect(self):
+ driver = self.get_webdriver()
+ if driver is not None:
+ self.login_sso(driver)
+ self.log.debug ("Connectig to SAP...")
+
+ return driver
+
+
+ def get_default_settings(self):
+ settings = {}
+ settings['CNF_SAP_SUser'] = 'SXXXXXXXXXX'
+ settings['CNF_SAP_SPass'] = 'MyP455w0rD'
+ settings['CNF_SAP_Login'] = LOGIN_PAGE_URL
+ settings['CNF_SAP_Logout'] = LOGOUT_PAGE_URL
+ settings['CNF_SAP_OData_Notes'] = ODATA_NOTE_URL
+ settings['CNF_SAP_Notes_URL'] = SAP_NOTE_URL
+ settings['CNF_SAP_CONN_TIMEOUT'] = TIMEOUT
+
+ return settings
+
+
+ def get_custom_settings(self):
+ settings = {}
+ settings['CNF_SAP_SUser'] = self.get_config_value('CNF_SAP_SUser')
+ settings['CNF_SAP_SPass'] = self.get_config_value('CNF_SAP_SPass')
+ settings['CNF_SAP_Login'] = self.get_config_value('CNF_SAP_Login')
+ settings['CNF_SAP_Logout'] = self.get_config_value('CNF_SAP_Logout')
+ settings['CNF_SAP_OData_Notes'] = self.get_config_value('CNF_SAP_OData_Notes')
+ settings['CNF_SAP_Notes_URL'] = self.get_config_value('CNF_SAP_Notes_URL')
+
+ return settings
+
+
+ def get_webdriver(self):
+ '''
+ FIXME: check supported webdrivers and return the webdriver
+ which best fits:
+ 1.- PhantomJS
+ 2.- Firefox
+ '''
+
+ driver = None
+ self.log.debug("Loading webdriver...")
+
+ phantomjs = which('phantomjs')
+
+ if phantomjs is not None:
+ driver = webdriver.PhantomJS()
+ self.log.debug("PhantomJS driver available")
+ else:
+ driver = webdriver.Firefox()
+ self.log.debug("Firefox driver available")
+
+ return driver
+
+ #~ if sys.platform == 'linux':
+ #~ # First we try with PhantomJS
+ #~ try:
+ #~ driver = webdriver.PhantomJS()
+ #~ self.log.debug("PhantomJS available")
+ #~ except:
+ #~ self.log.warning("PhantomJS not available")
+ #~ try:
+ #~ driver = webdriver.Firefox()
+ #~ self.log.debug ("Firefox available")
+ #~ except:
+ #~ self.log.error("Firefox not available")
+ #~ driver = None
+ #~ elif sys.platform == 'win32':
+ #~ try:
+ #~ driver = webdriver.Ie("C:\\windows\\system32\\IEDriverServer.exe")
+ #~ self.log.debug ("IE available")
+ #~ except:
+ #~ self.log.error("IE not available")
+ #~ driver = None
+ #~ return driver
+
+
+ def logout(self, driver):
+ driver.get(LOGOUT_PAGE_URL)
+ driver.quit()
+
+
+ def login_sso(self, driver):
+ '''
+ Login into SAP Support
+ '''
+ self.log.debug("Waiting for page...")
+ myuser = self.get_config_value('CNF_SAP_SUser')
+ mypass = self.get_config_value('CNF_SAP_SPass')
+ myloginpage = self.get_config_value('CNF_SAP_Login')
+
+ driver.get(myloginpage)
+ self.wait_for_page_load(driver)
+ self.log.debug("Page loaded")
+ username = driver.find_element_by_id('j_username')
+ username.send_keys(myuser)
+ password = driver.find_element_by_id('j_password')
+ password.send_keys(mypass)
+ login = driver.find_element_by_id('logOnFormSubmit')
+ login.click()
+ # FIXME: Wait time as editable property
+ #~ time.sleep(5)
+
+
+ def feedparser_parse(self, thing):
+ try:
+ return feedparser.parse(thing)
+ except TypeError:
+ if 'drv_libxml2' in feedparser.PREFERRED_XML_PARSERS:
+ feedparser.PREFERRED_XML_PARSERS.remove('drv_libxml2')
+ return feedparser.parse(thing)
+ else:
+ self.log.error(self.get_traceback())
+ raise
+
+
+
+ def get_notes(self):
+ '''
+ Return all sapnotes
+ '''
+ return self.sapnotes
+
+
+ def get_total(self):
+ '''
+ Return total sapnotes
+ '''
+ return len(self.sapnotes)
+
+
+ def load_notes(self):
+ '''
+ If notes.json exists, load notes
+ '''
+ try:
+ fnotes = self.get_file('SAP')
+ with open(fnotes, 'r') as fp:
+ self.sapnotes = json.load(fp)
+ self.log.debug ("Loaded %d notes" % len(self.sapnotes))
+ except Exception as error:
+ self.log.info("SAP Notes database not found. Creating a new one")
+ self.save_notes()
+
+
+ def get_node(self, sid):
+ try:
+ return self.sapnotes[sid]
+ except Exception as KeyError:
+ return None
+
+
+ def get_note(self, note):
+ '''
+ Get xml from SAP Support Notes service
+ '''
+ self.driver.get(ODATA_NOTE_URL % note)
+ return self.driver.page_source
+
+
+ def add_note(self, sapnote, content):
+ '''
+ Get header details from SAP Note
+ '''
+ try:
+ f= self.feedparser_parse(content)
+ f = feedparser.parse(content)
+ sid = f.entries[0].d_sapnotesnumber
+ self.sapnotes[sid] = {}
+ self.sapnotes[sid]['id'] = sid
+ self.sapnotes[sid]['componentkey'] = f.entries[0].d_componentkey
+ self.sapnotes[sid]['componenttxt'] = f.entries[0].d_componenttext
+ self.sapnotes[sid]['category'] = f.entries[0].d_category_detail['value']
+ self.sapnotes[sid]['language'] = f.entries[0].d_languagetext_detail['value']
+ self.sapnotes[sid]['title'] = f.entries[0].d_title_detail['value']
+ self.sapnotes[sid]['priority'] = f.entries[0].d_priority_detail['value']
+ self.sapnotes[sid]['releaseon'] = f.entries[0].d_releasedon
+ self.sapnotes[sid]['type'] = f.entries[0].d_type_detail['value']
+ self.sapnotes[sid]['version'] = f.entries[0].d_version_detail['value']
+ self.sapnotes[sid]['feedupdate'] = f.entries[0].updated
+ self.sapnotes[sid]['bookmark'] = False
+ self.log.info ("Added SAP Note %s" % sapnote)
+ return True
+ except Exception as error:
+ self.log.error("SAP Note %s could not be analyzed" % sapnote)
+ return False
+
+
+ def browse_notes(self, sapnotes):
+ driver = webdriver.Firefox()
+ self.login_sso(driver)
+ for sapnote in sapnotes:
+ self.log.debug("Browsing SAP Note %s" % sapnote)
+ body = driver.find_element_by_tag_name("body")
+ body.send_keys(Keys.CONTROL + 't')
+ driver.get(SAP_NOTE_URL % sapnote)
+ self.wait_for_page_load(driver)
+
+
+ def save_notes(self, filename='', bag={}):
+ '''
+ Save SAP Notes to file
+ '''
+ if len(filename) == 0:
+ filename = self.get_file('SAP')
+ bag = self.sapnotes
+
+ fnotes = open(filename, 'w')
+ json.dump(bag, fnotes)
+ fnotes.close()
+ self.log.info ("Saved %d notes to %s" % (len(bag), filename))
+
+
+
+
+ def apply_preferences(self, *args):
+ self.gui = self.app.get_service('GUI')
+ settings = self.get_default_settings()
+ new_settings = {}
+ for key in settings:
+ widget = self.gui.get_widget(key)
+ value = widget.get_text()
+ new_settings[key] = value
+ #~ self.log.debug(new_settings)
+ self.config[self.section] = new_settings
+ self.save_config()
+ self.log.debug("Settings saved")
+
+
+
+ def default_preferences(self, *args):
+ self.gui = self.app.get_service('GUI')
+ settings = self.get_default_settings()
+ for key in settings:
+ widget = self.gui.get_widget(key)
+ widget.set_text(settings[key])
+
+ #~ self.log.debug(settings)
+ self.config[self.section] = settings
+ self.save_config()
+ infobar = self.gui.get_widget('infobar')
+ infobar.set_markup("<b>Settings reverted to default</b>")
+
+
+ def get_linked_projects(self, sapnote):
+ try:
+ projects = self.sapnotes[sapnote]['projects']
+ except Exception as error:
+ projects = []
+
+ return projects
+
+
+ def get_linked_tasks(self, sapnote):
+ try:
+ tasks = self.sapnotes[sapnote]['tasks']
+ except Exception as error:
+ tasks = []
+ self.log.debug("Tasks: %s" % tasks)
+ return tasks
+
+
+ #~ def link_to_project(self, sapnotes, projects):
+ #~ for sapnote in sapnotes:
+ #~ try:
+ #~ self.sapnotes[sapnote]['projects'] = projects
+ #~ self.log.debug("Linked SAP Note %s to project(s): %s" % (sapnote, projects) )
+ #~ except:
+ #~ self.log.error(self.get_traceback())
+
+
+ def link_to_task(self, sapnotes, tasks):
+ for sapnote in sapnotes:
+ try:
+ self.sapnotes[sapnote]['tasks'] = tasks
+ self.log.info("Linked SAP Note %s to task(s): %s" % (sapnote, tasks) )
+ except:
+ self.log.error(self.get_traceback())
+
+
+ def import_sapnotes(self, bag):
+ for sid in bag:
+ # Check if SAP Note exists in main database
+ found = self.get_node(sid)
+ if found is None:
+ self.sapnotes[sid] = bag[sid]
+ else:
+ # Import only tasks
+ try:
+ imptasks = bag[sid]['tasks']
+ tasks = self.sapnotes[sid]['tasks']
+ tasks.extend(imptasks)
+ self.sapnotes[sid]['tasks'] = tasks
+ except: pass
+ # Import other metadata
+
+
+ def set_bookmark(self, sapnotes):
+ for sapnote in sapnotes:
+ self.log.info("SAP Note %s bookmarked: True" % sapnote)
+ self.sapnotes[sapnote]['bookmark'] = True
+ self.save_notes()
+
+
+ def set_no_bookmark(self, sapnotes):
+ for sapnote in sapnotes:
+ self.log.info("SAP Note %s bookmarked: False" % sapnote)
+ self.sapnotes[sapnote]['bookmark'] = False
+ self.save_notes()
+
+
+ def is_bookmark(self, sapnote):
+ try:
+ return self.sapnotes[sapnote]['bookmark']
+ except:
+ return False
+
+
+ def delete_sapnote(self, sapnote):
+ deleted = False
+ try:
+ del (self.sapnotes[sapnote])
+ deleted = True
+ except:
+ deleted = False
+
+ return deleted
+
+
+ def run(self):
+ self.load_notes()
+ self.build_stats()
+
+
+ def quit(self):
+ self.driver.quit()
+
+
+ def end(self):
+ self.save_notes()
+
Index: basico/env.py
===================================================================
--- basico/env.py (nonexistent)
+++ basico/env.py (revision 4)
@@ -0,0 +1,49 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: env.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Environment variables
+
+import sys
+import os
+from os.path import abspath, sep as SEP
+
+ROOT = abspath(sys.modules[__name__].__file__ + "/..")
+USER_DIR = os.path.expanduser('~')
+
+# App Info
+APP = {}
+APP['short'] = "basico"
+APP['name'] = "SAP Notes Manager for SAP Consultants\n\nThe code is licensed under the terms of the GPL v3 so you're free to grab, extend, improve and fork the code as you want"
+APP['version'] = "0.1"
+APP['authors'] = ["Tomás Vírseda <t00m@t00mlabs.net>"]
+APP['documenters'] = ["Tomás Vírseda <t00m@t00mlabs.net>"]
+APP['email'] = "t00m@t00mlabs.net"
+
+# Local paths
+LPATH = {}
+LPATH['ROOT'] = USER_DIR + SEP + '.basico' + SEP
+LPATH['ETC'] = LPATH['ROOT'] + 'etc' + SEP
+LPATH['VAR'] = LPATH['ROOT'] + 'var' + SEP
+LPATH['PLUGINS'] = LPATH['VAR'] + 'plugins' + SEP
+LPATH['LOG'] = LPATH['VAR'] + 'logs' + SEP
+LPATH['TMP'] = LPATH['VAR'] + 'tmp' + SEP
+LPATH['DB'] = LPATH['VAR'] + 'db' + SEP
+LPATH['EXPORT'] = LPATH['VAR'] + 'export' + SEP
+
+# Global paths
+GPATH = {}
+GPATH['DATA'] = ROOT + SEP + 'data' + SEP
+GPATH['UI'] = GPATH['DATA'] + 'ui' + SEP
+GPATH['ICONS'] = GPATH['DATA'] + 'icons' + SEP
+GPATH['PLUGINS'] = GPATH['DATA'] + 'plugins' + SEP
+GPATH['SHARE'] = GPATH['DATA'] + 'share' + SEP
+GPATH['DOC'] = GPATH['SHARE'] + 'docs' + SEP
+
+# Configuration, SAP Notes Database and Log files
+FILE = {}
+FILE['CNF'] = LPATH['ETC'] + 'basico.ini'
+FILE['SAP'] = LPATH['DB'] + 'sapnotes.json'
+FILE['LOG'] = LPATH['LOG'] + 'basico.log'
+FILE['CREDITS'] = GPATH['DOC'] + 'CREDITS'
/basico/env.py
Property changes:
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Index: basico/settings.py
===================================================================
--- basico/settings.py (nonexistent)
+++ basico/settings.py (revision 4)
@@ -0,0 +1,21 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: settings.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: Settings service
+
+
+from gi.repository import Gtk
+from gi.repository import Gio
+from gi.repository import Pango
+from gi.repository.GdkPixbuf import Pixbuf
+
+from .service import Service
+
+class Settings(Service):
+ def initialize(self):
+ #~ self.gui = self.app.get_service('GUI')
+ #~ sapnoteview = self.gui.get_widget('sapnoteview')
+ view = self.get_config_value('View')
+ #~ sapnoteview.set_view(view)
Index: basico/sapnoteview.py
===================================================================
--- basico/sapnoteview.py (nonexistent)
+++ basico/sapnoteview.py (revision 4)
@@ -0,0 +1,951 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# File: sapnoteview.py
+# Author: Tomás Vírseda
+# License: GPL v3
+# Description: TreeView for SAP Notes
+
+from cgi import escape
+from enum import IntEnum
+from gi.repository import Gtk
+from gi.repository.GdkPixbuf import Pixbuf
+from gi.repository import Pango
+from datetime import datetime
+from dateutil import parser as dateparser
+
+from .log import get_logger
+
+#~ Enum(value='Column', names=<...>, *, module='...', qualname='...', type=<mixed-in class>, start=1)
+#~ Column = Enum('Column', 'rowtype checkbox icon id title component category type priority language released', start=0)
+
+class Column(IntEnum):
+ rowtype = 0
+ checkbox = 1
+ icon = 2
+ id = 3
+ title = 4
+ component = 5
+ category = 6
+ type = 7
+ priority = 8
+ language = 9
+ released = 10
+
+
+class SAPNoteView(Gtk.TreeView):
+ def __init__(self, app):
+ self.app = app
+ LOG_FILE = self.app.get_file('LOG')
+ LOG_NAME = self.__class__.__name__
+ self.log = get_logger(LOG_NAME, LOG_FILE)
+ self.get_services()
+ self.toggled = 0
+ self.selected = set()
+ self.count = 0
+
+ try:
+ self.view = self.settings.get_config_value('View')
+ except:
+ self.set_view('components')
+
+
+ # Setup treeview and model
+ Gtk.TreeView.__init__(self)
+ self.model = Gtk.TreeStore(
+ str, # RowType
+ bool, # CheckBox
+ Pixbuf, # Icon
+ str, # Component key
+ str, # Category
+ str, # Type
+ str, # SAP Note Id
+ str, # Title
+ str, # Priority
+ str, # Language
+ str # Release date
+ )
+ self.set_model(self.model)
+
+ # Setup columns
+ # RowType
+ renderer = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn('', renderer, text=Column.rowtype.value)
+ column.set_visible(False)
+ column.set_expand(False)
+ column.set_clickable(False)
+ column.set_sort_indicator(False)
+ self.append_column(column)
+
+ # Checkbox
+ renderer = Gtk.CellRendererToggle()
+ renderer.connect("toggled", self.on_cell_toggled)
+ column = Gtk.TreeViewColumn('X', renderer, active=1)
+ column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column.set_visible(True)
+ column.set_expand(False)
+ column.set_clickable(False)
+ column.set_sort_indicator(False)
+ self.append_column(column)
+ column.set_cell_data_func(renderer, self.cell_data_func)
+
+ # Icon
+ renderer = Gtk.CellRendererPixbuf()
+ renderer.set_alignment(0.0, 0.5)
+ self.column_icon = Gtk.TreeViewColumn('', renderer, pixbuf=2)
+ self.column_icon.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ self.column_icon.set_visible(True)
+ self.column_icon.set_expand(False)
+ self.column_icon.set_clickable(False)
+ self.column_icon.set_sort_indicator(False)
+ self.append_column(self.column_icon)
+
+ # Component key
+ renderer = Gtk.CellRendererText()
+ #~ renderer.set_property('ellipsize', Pango.EllipsizeMode.MIDDLE)
+ self.column_component = Gtk.TreeViewColumn('SAP Notes', renderer, markup=3)
+ self.column_component.set_visible(True)
+ #~ self.column_component.set_expand(True)
+ self.column_component.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ self.column_component.set_clickable(True)
+ self.column_component.set_sort_indicator(True)
+ self.column_component.set_sort_column_id(3)
+ self.column_component.set_sort_order(Gtk.SortType.ASCENDING)
+ self.append_column(self.column_component)
+ expander_column = self.column_component
+
+
+ # Category
+ renderer = Gtk.CellRendererText()
+ self.column_cat = Gtk.TreeViewColumn('Category', renderer, markup=4)
+ self.column_cat.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ self.column_cat.set_visible(False)
+ self.column_cat.set_expand(False)
+ self.column_cat.set_clickable(True)
+ self.column_cat.set_sort_indicator(True)
+ self.column_cat.set_sort_column_id(4)
+ self.column_cat.set_sort_order(Gtk.SortType.ASCENDING)
+ self.append_column(self.column_cat)
+
+ # Type
+ renderer = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn('Type', renderer, markup=5)
+ column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column.set_visible(False)
+ column.set_expand(False)
+ column.set_clickable(True)
+ column.set_sort_indicator(True)
+ column.set_sort_column_id(5)
+ column.set_sort_order(Gtk.SortType.ASCENDING)
+ self.append_column(column)
+
+ # SAP Note Id
+ renderer = Gtk.CellRendererText()
+ self.column_sid = Gtk.TreeViewColumn('SAP Note', renderer, markup=6)
+ self.column_sid.set_visible(False)
+ self.column_sid.set_expand(False)
+ self.column_sid.set_clickable(True)
+ self.column_sid.set_sort_indicator(True)
+ self.column_sid.set_sort_column_id(6)
+ self.column_sid.set_sort_order(Gtk.SortType.ASCENDING)
+ self.append_column(self.column_sid)
+
+ # Title
+ renderer = Gtk.CellRendererText()
+ renderer.set_property('ellipsize', Pango.EllipsizeMode.MIDDLE)
+ self.column_title = Gtk.TreeViewColumn('Title', renderer, markup=7)
+ self.column_title.set_visible(False)
+ self.column_title.set_expand(True)
+ self.column_title.set_clickable(True)
+ self.column_title.set_sort_indicator(True)
+ self.column_title.set_sort_column_id(7)
+ self.column_title.set_sort_order(Gtk.SortType.ASCENDING)
+ self.append_column(self.column_title)
+
+ # Priority
+ renderer = Gtk.CellRendererText()
+ self.column_priority = Gtk.TreeViewColumn('Priority', renderer, markup=8)
+ self.column_priority.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ self.column_priority.set_visible(True)
+ self.column_priority.set_expand(False)
+ self.column_priority.set_clickable(True)
+ self.column_priority.set_sort_indicator(True)
+ self.column_priority.set_sort_column_id(8)
+ self.column_priority.set_sort_order(Gtk.SortType.ASCENDING)
+ self.append_column(self.column_priority)
+
+ # Language
+ renderer = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn('Language', renderer, markup=9)
+ column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column.set_visible(False)
+ column.set_expand(False)
+ column.set_clickable(True)
+ column.set_sort_indicator(True)
+ column.set_sort_column_id(9)
+ column.set_sort_order(Gtk.SortType.ASCENDING)
+ self.append_column(column)
+
+ # Release date
+ renderer = Gtk.CellRendererText()
+ self.column_rel = Gtk.TreeViewColumn('Released on', renderer, markup=10)
+ self.column_rel.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ self.column_rel.set_visible(False)
+ self.column_rel.set_expand(False)
+ self.column_rel.set_clickable(True)
+ self.column_rel.set_sort_indicator(True)
+ self.column_rel.set_sort_column_id(10)
+ self.column_rel.set_sort_order(Gtk.SortType.ASCENDING)
+ self.append_column(self.column_rel)
+
+ # TreeView common
+ self.set_level_indentation(10)
+ self.set_can_focus(True)
+ self.set_headers_visible(False)
+ self.set_enable_search(True)
+ #~ self.set_rules_hint(True)
+ #~ self.set_expander_column(expander_column)
+ self.set_hover_selection(True)
+ self.set_grid_lines(Gtk.TreeViewGridLines.NONE)
+ self.set_search_entry(self.gui.get_widget('stySearchInfo'))
+ self.set_search_column(3)
+ #~ self.set_row_separator_func(self.row_separator_func)
+
+ # Selection
+ self.selection = self.get_selection()
+ self.selection.set_select_function(self.select_function)
+ self.selection.set_mode(Gtk.SelectionMode.BROWSE)
+
+ # Go live
+ self.connect('button_press_event', self.cb_button_press)
+ self.connect('row-activated', self.double_click)
+ self.connect('cursor-changed', self.row_changed)
+ self.show_all()
+
+
+ def cell_data_func(self, column, cell, store, iter, data=None):
+ rowtype = store[iter][0]
+ if rowtype != 'sapnote':
+ cell.set_visible(False)
+ else:
+ cell.set_visible(True)
+
+
+ def select_function(self, selection, store, path, current):
+ return True
+
+ #~ This code works: only allow select rows of type 'sapnote'
+ #~ rowtype = store[path][0]
+ #~ if rowtype == 'sapnote':
+ #~ return True
+ #~ else:
+ #~ return False
+
+
+ #~ def row_separator_func(self, model, iter):
+ #~ """ Call user function to determine if this node is separator """
+ #~ if iter and model.iter_is_valid(iter):
+ #~ rowtype = model.get_value(iter, 0)
+ #~ if row<type == 'sapnote':
+ #~ return True
+ #~ else:
+ #~ return False
+ #~ self.log.debug("Row type: %s" % rowtype)
+ #~ return False
+ #~ return False
+
+
+
+ def get_services(self):
+ self.gui = self.app.get_service("GUI")
+ self.cb = self.app.get_service('Callbacks')
+ self.menu = self.app.get_service("Menus")
+ self.sap = self.app.get_service('SAP')
+ self.im = self.app.get_service('IM')
+ self.settings = self.app.get_service('Settings')
+ self.plugins = self.app.get_service('Plugins')
+
+
+ def row_changed(self, treeview):
+ #~ button = self.gui.get_widget('mnuBtnActions')
+ selected = set()
+ selection = treeview.get_selection()
+ model, treeiters = selection.get_selected_rows()
+
+
+ def double_click(self, treeview, row, col):
+ model = treeview.get_model()
+ noteid = model[row][6]
+ self.set_select_notes([noteid])
+ #~ self.log.debug("Launching SAP Note %s in browser" % noteid)
+ self.cb.actions_browse()
+
+
+ def cb_button_press(self, treeview, event, data=None):
+ if event.button == 3:
+ x = int(event.x)
+ y = int(event.y)
+ time = event.time
+ pthinfo = self.get_path_at_pos(x,y)
+ if pthinfo is not None:
+ path,col,cellx,celly = pthinfo
+ self.grab_focus()
+ self.set_cursor(path,col,0)
+
+ model = treeview.get_model()
+ treeiter = model.get_iter(path)
+ rowtype = model.get_value(treeiter, 0)
+
+ if rowtype == 'task':
+ task = model.get_value(treeiter, 6)
+ self.popup_menu = self.menu.create_popup_menu_by_task(task)
+ elif rowtype == 'component':
+ component = model.get_value(treeiter, 6)
+ self.popup_menu = self.menu.create_popup_menu_by_component(component)
+ elif rowtype == 'sapnote':
+ sid = model.get_value(treeiter, 6)
+ self.popup_menu = self.menu.create_popup_menu_by_sapnote(sid)
+ else:
+ return False
+
+
+ #~ noteid = model.get_value(treeiter, 6)
+ #~ sapnote = self.sap.get_node(noteid)
+ #~ self.log.debug("Selected SAP Note: %s" % sapnote)
+
+ self.popup_menu.show_all()
+ self.popup_menu.popup(None, None, None, None, event.button, event.time)
+ return True # event has been handled
+
+
+ def populate(self, sapnotes):
+ #~ self.column_icon.set_visible(True)
+ #~ self.column_component.set_visible(True)
+ #~ self.column_title.set_visible(False)
+ #~ self.column_cat.set_visible(False)
+
+ self.log.debug("View '%s' populated with %d SAP Notes" % (self.view, len(sapnotes)))
+ self.set_headers_visible(False)
+ self.column_sid.set_visible(False)
+ self.column_rel.set_visible(False)
+ self.column_priority.set_visible(False)
+ self.column_icon.set_visible(True)
+
+ if self.view == 'components':
+ self.populate_by_components(sapnotes)
+ elif self.view == 'projects':
+ self.populate_by_projects(sapnotes)
+ elif self.view == 'tasks':
+ self.populate_by_tasks(sapnotes)
+ elif self.view == 'bookmarks':
+ self.populate_by_bookmarks(sapnotes)
+ else:
+ self.populate_by_components(sapnotes)
+
+ # Save last populated list of sap notes
+ total = self.sap.get_total()
+ notesid = set()
+ for sapnote in sapnotes:
+ notesid.add(sapnote)
+
+ if len(notesid) < total:
+ self.settings.set_config_value('Notes', ','.join(notesid))
+
+
+
+ def populate_by_bookmarks(self, sapnotes):
+ #~ self.log.debug('Populating bookmarks')
+ self.model.clear()
+ self.set_headers_visible(True)
+ self.column_sid.set_visible(True)
+ self.column_icon.set_visible(False)
+ self.column_component.set_visible(True)
+ self.column_component.set_expand(False)
+ self.column_title.set_visible(True)
+ self.column_priority.set_expand(False)
+ self.column_cat.set_visible(False)
+ #~ self.column_component.set_visible(False)
+ #~ self.column_component.set_expand(False)
+ self.column_component.set_title('Component')
+ icon_bookmark = self.im.get_icon('bookmark')
+
+ for sid in sapnotes:
+ try:
+ bookmark = sapnotes[sid]['bookmark'] # True or False
+ except:
+ bookmark = False
+
+ if bookmark:
+ sapnote = self.get_node_sapnote(sapnotes[sid], sid)
+ self.model.append(None, sapnote)
+
+
+ def get_node_project(self, project, icon):
+ if project == '':
+ title = "<span size='18000'><b>No project assigned</b></span>"
+ else:
+ title = "<span size='18000'><b>%s</b></span>" % project
+
+ node = []
+ node.append('project')
+ node.append(0)
+ node.append(icon)
+ node.append(title) # Component
+ node.append("") # Category
+ node.append("") # Type
+ node.append("") # Id
+ node.append("") # Title
+ node.append("") # Priority
+ node.append("") # Lang
+ node.append("") # Release on
+ return node
+
+
+ def get_node_task(self, task, icon):
+ if task == '':
+ title = "<span size='18000'><b>No task assigned</b></span>"
+ else:
+ title = "<span size='18000'><b>%s</b></span>" % task
+
+ node = []
+ node.append('task')
+ node.append(0)
+ node.append(icon)
+ node.append(title) # Component
+ node.append("") # Category
+ node.append("") # Type
+ node.append(task) # Id
+ node.append("") # Title
+ node.append("") # Priority
+ node.append("") # Lang
+ node.append("") # Release on
+ return node
+
+
+ def populate_by_projects(self, sapnotes):
+ self.model.clear()
+ treepids = {}
+ icon_noproject = self.im.get_icon('noproject')
+ icon_project = self.im.get_icon('project')
+ icon_sapnote = self.im.get_icon('sapnote')
+ icon_bookmark = self.im.get_icon('bookmark')
+ self.column_component.set_title('Projects')
+
+ if len(sapnotes) == 0:
+ return
+
+ node = self.get_node_project('', icon_noproject)
+ pid = self.model.append(None, node)
+ treepids['None'] = pid
+
+ notlinked = 0
+ for sid in sapnotes:
+ try:
+ projects = sapnotes[sid]['projects']
+ except:
+ projects = []
+
+
+ if len(projects) == 0:
+ #~ SAP Note not linked to any project
+ sapnote = self.get_node_sapnote(sapnotes[sid], sid)
+ self.model.append(treepids['None'], sapnote)
+ notlinked += 1
+ else:
+ #~ SAP Note linked to projects
+ for project in projects:
+ try:
+ pid = treepids[project]
+ except:
+ node = self.get_node_project(project, icon_project)
+ pid = self.model.append(None, node)
+ treepids[project] = pid
+
+ sapnote = self.get_node_sapnote(sapnotes[sid], sid)
+ self.model.append(pid, sapnote)
+
+ if notlinked == 0:
+ self.model.remove(treepids['None'])
+
+
+ def populate_by_tasks(self, sapnotes):
+ self.model.clear()
+ treepids = {}
+ icon_notask = self.im.get_icon('notask')
+ icon_task = self.im.get_icon('task')
+ icon_sapnote = self.im.get_icon('sapnote')
+ icon_bookmark = self.im.get_icon('bookmark')
+ self.column_component.set_title('Tasks')
+
+ if len(sapnotes) == 0:
+ return
+
+ #~ "No task assigned" node creation
+ node = self.get_node_task('', icon_notask)
+ pid = self.model.append(None, node)
+ treepids['None'] = pid
+
+ scomp = set()
+ dcomp = {}
+ taskset = set()
+ for sid in sapnotes:
+ try:
+ # setup components
+ compkey = escape(sapnotes[sid]['componentkey'])
+ comptxt = escape(sapnotes[sid]['componenttxt'])
+ scomp.add(compkey)
+ dcomp[compkey] = comptxt
+
+ # setup tasks
+ for task in sapnotes[sid]['tasks']:
+ taskset.add(task)
+ except: pass
+
+ lcomp = list(scomp)
+ lcomp.sort()
+
+ tasklist = []
+ tasklist.extend(taskset)
+ tasklist.sort()
+
+ for task in tasklist:
+ node = self.get_node_task(task, icon_task)
+ pid = self.model.append(None, node)
+ treepids[task] = pid
+
+ notask = 0
+ for sid in sapnotes:
+ #~ Get category
+ compkey = escape(sapnotes[sid]['componentkey'])
+ catname = escape(sapnotes[sid]['category'])
+
+
+ #~ Get tasks for this sapnote
+ try:
+ tasks = sapnotes[sid]['tasks']
+ except:
+ sapnotes[sid]['tasks'] = tasks = []
+
+
+ if len(tasks) == 0:
+ # Components in no-task node
+ cmpkey = "notask" + '-' + compkey
+ comptxt = dcomp[compkey]
+ try:
+ pid = treepids[cmpkey]
+ except:
+ node = self.get_node_component(compkey, comptxt)
+ pid = self.model.append(treepids['None'], node)
+ treepids[cmpkey] = pid
+
+ # Categories in no-task node
+ catkey = "notask" + '-' + compkey + '-' + catname
+ try:
+ cid = treepids[catkey]
+ except:
+ node = self.get_node_category(sapnotes[sid])
+ cid = self.model.append(pid, node)
+ treepids[catkey] = cid
+
+ #~ SAP Note not linked to any task
+ sapnote = self.get_node_sapnote(sapnotes[sid], sid)
+ self.model.append(cid, sapnote)
+ notask += 1
+ else:
+ #~ SAP Note linked to tasks
+ for task in sapnotes[sid]['tasks']:
+ # Components in task node
+ cmpkey = task + '-' + compkey
+ comptxt = dcomp[compkey]
+ try:
+ pid = treepids[cmpkey]
+ except:
+ node = self.get_node_component(compkey, comptxt)
+ pid = self.model.append(treepids[task], node)
+ treepids[cmpkey] = pid
+
+ catkey = task + '-' + compkey + '-' + catname
+ try:
+ cid = treepids[catkey]
+ except:
+ node = self.get_node_category(sapnotes[sid])
+ cid = self.model.append(pid, node)
+ treepids[catkey] = cid
+
+ pid = treepids[task]
+ sapnote = self.get_node_sapnote(sapnotes[sid], sid)
+ self.model.append(cid, sapnote)
+
+ #~ "No task assigned" node deletion if no tasks at all
+ if notask == 0:
+ self.model.remove(treepids['None'])
+
+
+ def get_node_component(self, compkey, comptxt):
+ #~ compkey = escape(sapnote['componentkey'])
+ #~ comptxt = escape(sapnote['componenttxt'])
+ icon = self.im.get_icon('component')
+ node = []
+ component = "<big><b>%s</b></big> (<i>%s</i>)" % (compkey, comptxt) # component
+ #~ component = "<big><b>%s</b></big>" % (compkey)
+ #~ component = "<span size='18000'><b>%s</b></span> <span size='12000'>(<i>%s</i>)</span>" % (compkey, comptxt)
+ node.append('component')
+ node.append(0)
+ node.append(icon)
+ node.append(component) # Component
+ node.append("") # Category
+ node.append("") # Type
+ node.append(compkey) # Id
+ node.append("") # Title
+ node.append("") # Priority
+ node.append("") # Lang
+ #~ node.append("") # Version
+ node.append("") # Release on
+ return node
+
+
+ def get_node_category(self, sapnote):
+ compkey = escape(sapnote['componentkey'])
+ catname = escape(sapnote['category'])
+ catkey = compkey + '-' + catname
+ icon = self.im.get_icon('category')
+
+ if len(catname) == 0:
+ #~ catname = "<span size='12000'><b>No category assigned</b></span>"
+ catname = "\t<big><b>No category assigned</b></big>"
+ else:
+ #~ category = "<span size='15000'><b>%s</b></span>" % catname
+ category = "\t<big><b>%s</b></big>" % catname
+
+ node = []
+ node.append('category')
+ node.append(0)
+ node.append(icon)
+ node.append(category) # Component
+ node.append("") # Category
+ node.append("") # Type
+ node.append("") # Id
+ node.append("") # Title
+ node.append("") # Priority
+ node.append("") # Lang
+ #~ node.append("") # Version
+ node.append("") # Release on
+ return node
+
+
+ def get_node_sapnote(self, sapnote, sid):
+ compkey = escape(sapnote['componentkey'])
+ icon_note = self.im.get_pixbuf_icon('sapnote')
+ icon_fav = self.im.get_icon('bookmark')
+ compkey = escape(sapnote['componentkey'])
+ compkey = escape(sapnote['componentkey'])
+ catname = escape(sapnote['category'])
+ sn = "%10d" % (int(sid)) # SAP Note Id (000000nnnn)
+
+ # Get bookmark
+ try:
+ bookmark = sapnote['bookmark']
+ except Exception as error:
+ bookmark = False
+
+ # Get correct title
+ if self.get_view() == 'bookmarks':
+ title = "<big>%s</big>" % (escape(sapnote['title']))
+ elif self.get_view() == 'components':
+ title = "\t\t<big>SAP Note <b>%s</b> - %s</big>" % (sn, escape(sapnote['title']))
+ else:
+ title = "\t\t<big><b>SAP Note %s</b> - %s</big>" % (sn, escape(sapnote['title']))
+
+ node = []
+ # 0. # RowType
+ node.append('sapnote')
+
+ # 1. # CheckBox
+ node.append(0)
+
+ # 2. # Icon
+ if bookmark:
+ node.append(icon_fav)
+ else:
+ node.append(icon_note)
+ #~ node.append(None)
+
+ # 3. # Component
+ if self.view == 'components':
+ node.append(title)
+ # 4. # Category
+ node.append("")
+ elif self.view == 'bookmarks':
+ node.append("<big>%s</big>" % compkey)
+ # 4. # Category
+ node.append("%s" % catname)
+ #~ node.append(catname)
+ else:
+ node.append(title)
+ # 4. # Category
+ node.append("")
+
+ # 5. # Type
+ node.append(sapnote['type'])
+
+ # 6. # Sap Note ID
+ node.append("%s" %sid)
+
+ # 7. # Title
+ if self.view == 'bookmarks':
+ node.append(title)
+ else:
+ node.append("")
+
+ # 8. # Priority
+ priority = "%s" % escape(sapnote['priority'])
+ node.append(priority)
+
+ # 9. # Lang
+ node.append(sapnote['language'])
+
+ # 10. # Release date
+ released = dateparser.parse(sapnote['releaseon'])
+ node.append(released.strftime("%Y.%m.%d"))
+
+ return node
+
+
+ def populate_by_components(self, sapnotes, only_bookmarks=False):
+ #~ self.log.debug('Populating by components')
+ self.model.clear()
+ self.treepids = {}
+ self.column_component.set_title('Components')
+ self.column_sid.set_visible(False)
+ self.column_title.set_visible(False)
+ self.set_headers_visible(False)
+ self.set_grid_lines(Gtk.TreeViewGridLines.VERTICAL)
+
+ self.column_title
+
+ if len(sapnotes) == 0:
+ return
+
+ scomp = set()
+ dcomp = {}
+
+ for sid in sapnotes:
+ compkey = escape(sapnotes[sid]['componentkey'])
+ comptxt = escape(sapnotes[sid]['componenttxt'])
+ scomp.add(compkey)
+ dcomp[compkey] = comptxt
+ lcomp = list(scomp)
+ lcomp.sort()
+
+ for compkey in lcomp:
+ node = self.get_node_component(compkey, dcomp[compkey])
+ pid = self.model.append(None, node)
+ self.treepids[compkey] = pid
+
+ for sid in sapnotes:
+ #~ Gety component
+ compkey = escape(sapnotes[sid]['componentkey'])
+ #~ try:
+ pid = self.treepids[compkey]
+ #~ except:
+ #~ node = self.get_node_component(sapnotes[sid])
+ #~ pid = self.model.append(None, node)
+ #~ self.treepids[compkey] = pid
+
+ #~ Get category
+ catname = escape(sapnotes[sid]['category'])
+ catkey = compkey + '-' + catname
+ try:
+ cid = self.treepids[catkey]
+ except:
+ node = self.get_node_category(sapnotes[sid])
+ cid = self.model.append(pid, node)
+ self.treepids[catkey] = cid
+
+ #~ Get SAP Note
+ if only_bookmarks:
+ try:
+ bookmark = sapnotes[sid]['bookmark'] # True or False
+ except:
+ bookmark = False
+ if bookmark:
+ node = self.get_node_sapnote(sapnotes[sid], sid)
+ self.model.append(cid, node)
+ else:
+ node = self.get_node_sapnote(sapnotes[sid], sid)
+ self.model.append(cid, node)
+
+
+ def changed(self, *args):
+ try:
+ model, treeiters = self.selection.get_selected_rows()
+ selected = set()
+ if len(treeiters) > 0:
+ for treeiter in treeiters:
+ if treeiter != None:
+ selected.add(model[treeiter][0])
+ except Exception as error:
+ self.log.error (self.get_traceback())
+
+
+ def on_cell_toggled(self, widget, path):
+ model = self.get_model()
+ rtype = model[path][0]
+ if rtype != 'component' and rtype != 'category':
+ model[path][1] = not model[path][1]
+ self.count = 0
+ self.check_states()
+
+
+ def check_states(self):
+ lblSelectedNotes = self.gui.get_widget('lblSelectedNotes')
+ self.selected = set()
+
+ def traverse_treeview(model, path, iter, user_data=None):
+ rtype = model.get_value(iter, 0)
+ if rtype == 'component':
+ # select everything (categories and notes)
+ component = rtype = model.get_value(iter, 3)
+ ce = component.find('</b></big>')
+ compkey = component[8:ce]
+ elif rtype == 'category':
+ # select all notes under that category
+ category = rtype = model.get_value(iter, 3)
+ ce = category.find("</b></big>")
+ catkey = category[9:ce]
+
+ toggled = model.get_value(iter, 1)
+ if toggled:
+ self.count += 1
+ title = model.get_value(iter, 3)
+ sapnote = model.get_value(iter, 6)
+ self.selected.add(sapnote)
+ return False
+
+ model = self.get_model()
+ model.foreach(traverse_treeview)
+ lblSelectedNotes.set_markup('<big>%d of %d\nSAP Notes selected</big>' % (len(self.selected), self.sap.get_total()))
+ actions = self.gui.get_widget('mnuBtnActions')
+ if (len(self.selected)) > 0:
+ actions.set_sensitive(True)
+ self.cb.setup_menu_actions()
+ else:
+ actions.set_sensitive(False)
+
+
+ def set_select_notes(self, sapnotes):
+ self.selected = set()
+ for sapnote in sapnotes:
+ self.selected.add(sapnote)
+
+
+ def get_selected_notes(self):
+ return self.selected
+
+
+ def get_view(self):
+ if self.view is None:
+ return 'components'
+
+ return self.view
+
+
+ def set_view(self, view):
+ settings = self.app.get_service('Settings')
+
+ if view is None:
+ view = settings.get_config_value('View')
+
+ # Save view to settings
+ if view not in ['settings', 'download']:
+ self.view = view
+ settings = self.app.get_service('Settings')
+ settings.set_config_value('View', view)
+
+ # Change icon
+ iconview = self.gui.get_widget('imgViewCurrent')
+ if view == 'settings':
+ iconname = 'gtk-preferences'
+ elif view == 'download':
+ iconname = 'download'
+ else:
+ iconname = view
+
+ icon = self.im.get_pixbuf_icon(iconname, 48, 48)
+ iconview.set_from_pixbuf(icon)
+
+ # Change label
+ viewlabel = self.gui.get_widget('lblViewCurrent')
+ name = "<span size='20000'><b>%-10s</b></span>" % view.capitalize()
+ viewlabel.set_markup(name)
+
+
+
+ def expand(self):
+ switch = self.gui.get_widget('schExpandCollapse')
+ switch.set_active(True)
+
+ def collapse(self):
+ switch = self.gui.get_widget('schExpandCollapse')
+ switch.set_active(False)
+
+ def expand_collapse(self, switch, active):
+ if active:
+ self.expand_all()
+ else:
+ self.collapse_all()
+
+
+ def select_all_none(self, switch, active):
+ model = self.get_model()
+
+ def traverse_treeview(model, path, iter, user_data=None):
+ rtype = model.get_value(iter, 0)
+ if rtype == 'sapnote':
+ model.set_value(iter, 1, active)
+
+ return False
+
+ model.foreach(traverse_treeview, active)
+ self.check_states()
+
+
+ def select_by_component(self, component_target, active):
+ model = self.get_model()
+
+ def traverse_treeview(model, path, iter, user_data=None):
+ rtype = model.get_value(iter, 0)
+ if rtype == 'sapnote':
+ sid = model.get_value(iter, 6)
+ sapnote = self.sap.get_node(sid)
+ component_source = sapnote['componentkey']
+ #~ self.log.debug ("%s - %s" % (component_source, component_target))
+ #~ if component_source.startswith(component_target):
+ if component_source == component_target:
+ model.set_value(iter, 1, active)
+
+ return False
+
+ model.foreach(traverse_treeview, True)
+ self.check_states()
+
+
+ def select_by_task(self, task_target, active):
+ model = self.get_model()
+
+ def traverse_treeview(model, path, iter, user_data=None):
+ rtype = model.get_value(iter, 0)
+ if rtype == 'sapnote':
+ sid = model.get_value(iter, 6)
+ sapnote = self.sap.get_node(sid)
+ tasks_source = sapnote['tasks']
+
+ if task_target in tasks_source:
+ model.set_value(iter, 1, active)
+ else:
+ if len(task_target) == 0 and len(tasks_source) == 0:
+ model.set_value(iter, 1, active)
+
+ return False
+
+ model.foreach(traverse_treeview, True)
+ self.check_states()