Source code for main

# -*- coding: utf-8 -*-
# KrySA - Statistical analysis for rats
# Version: 0.3.7
# Copyright (C) 2016, KeyWeeUsr(Peter Badida) <keyweeusr@gmail.com>
# License: GNU GPL v3.0, More info in LICENSE.txt

from kivy.config import Config
import os

# local & RTD docs fix
if not any(['BUILDDIR' in os.environ, 'READTHEDOCS' in os.environ]):
    Config.set('graphics', 'window_state', 'maximized')
    import numpy
    import scipy

import re
import json
import math
import string
import sqlite3
import os.path as op
from kivy.app import App
from kivy.clock import Clock
from functools import partial
from dropdown import DropDown
from kivy.logger import Logger
from kivy.uix.popup import Popup
from kivy.uix.image import Image
from kivy.uix.label import Label
from time import gmtime, strftime
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.textinput import TextInput
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
from kivy.uix.recycleview import RecycleView
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.stencilview import StencilView
from kivy.uix.tabbedpanel import TabbedPanelItem
from kivy.uix.treeview import TreeView, TreeViewLabel
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.properties import StringProperty, ObjectProperty, \
    BooleanProperty, ListProperty

from tasks import Task
from tasks.basic import Basic
from tasks.avgs import Avgs
from tasks.manipulate import Manipulate


[docs]class ResultGrid(GridLayout): '''A black gridlayout, together with :mod:`main.Wrap` makes a table container for results that need a table. .. versionadded:: 0.3.2 '''
[docs]class Wrap(Label): '''A white label with automatically wrapped text. .. versionadded:: 0.3.2 ''' background_color = ListProperty([0, 0, 0, 0])
[docs]class PageBox(BoxLayout): '''A layout that includes Page widget together with transparent separator. It's used for adding new results from Tasks. .. versionadded:: 0.2.0 ''' def __init__(self, **kwargs): super(PageBox, self).__init__(**kwargs) self.add_widget = self.ids.page.add_widget
[docs]class PaperLabel(Label): '''A label with visual properties as a paper sheet. .. versionadded:: 0.2.0 '''
[docs]class ImgButton(Button): '''A button with an image of square shape in the middle. .. versionadded:: 0.2.0 ''' source = StringProperty('')
[docs]class ErrorPop(Popup): '''An error popup to let user know something is missing or typed wrong when console is disabled. .. versionadded:: 0.1.2 ''' message = StringProperty('') def __init__(self, **kw): self.message = kw.get('msg', '') super(ErrorPop, self).__init__(**kw)
[docs]class NewDataValue(BoxLayout): '''A layout handling the behavior of inputs and button for each new value in :ref:`data`. .. versionadded:: 0.1.4 ''' def __init__(self, **kw): self.app = App.get_running_app() super(NewDataValue, self).__init__(**kw) self.filter = kw['filter']
[docs]class NewDataColumn(BoxLayout): '''A layout handling the behavior of type, values(``NewDataValue``) and some buttons for each new column in :ref:`data`. .. versionadded:: 0.1.4 ''' def __init__(self, **kw): self.app = App.get_running_app() super(NewDataColumn, self).__init__(**kw) @staticmethod
[docs] def free(items): '''Frees all locked cells in the column except a column type. If a wrong type is used, removing the whole column is necessary. (protection against corrupting :ref:`sqlite`) .. versionadded:: 0.1.4 ''' for item in items: if hasattr(item, 'disabled'): try: if item.parent.ids['coltype'] == item: continue except: item.disabled = False else: # enable children for i in item: try: if i.parent.ids['coltype'] == item: continue except: i.disabled = False
[docs] def checklock(self, disable, coltype, check, *args): '''Disables all cells in the column, then check them against a list of strings that could be used to corrupt :ref:`sqlite` . If the check is done without an error, another check is made to protect against using an empty string ``''`` as a value, which if used inappropriately results in a crash. .. versionadded:: 0.1.4 ''' msg = 'Please remove any SQL keyword present in the column!' for item in disable: if hasattr(item, 'disabled'): item.disabled = True else: # disable children for i in item: i.disabled = True # check for any sql keywords and correct values for item in check: try: for keyword in self.app.sql_blacklist: if keyword in item.text.upper(): error = ErrorPop(msg=msg) error.open() return except AttributeError: for i in item.children: for keyword in self.app.sql_blacklist: if keyword in i.ids.value.text.upper(): error = ErrorPop(msg=msg) error.open() return try: if not coltype.text == 'TEXT': float(i.ids.value.text) except ValueError: if coltype.text == 'REAL': i.ids.value.text = '0.0' elif coltype.text == 'INTEGER': i.ids.value.text = '0'
[docs] def paste(self, values, sep): '''Paste a value(s) from a user's clipboard as a column values. A user can choose what kind of separator was used on the values, for example:: 1 2 3 4 5 # (space) 1\\t2\\t3\\t4\\t # (tab) 1\\n2\\n3\\n4\\n # Unix-like new line character (<enter>/<return>) If in doubt and your values were copied from a column (e.g. spreadsheet), use `OS default`, which will choose between ``\\n`` (Unix-like) or ``\\r\\n`` (Windows) new line separators. .. versionadded:: 0.3.4 ''' if sep == 'space': values = values.split(' ') elif sep == 'OS default': values = values.split(os.linesep) else: values = values.split(sep[1:]) for val in values: v = NewDataValue(filter=self.filter) v.ids.value.text = val self.ids.vals.add_widget(v) self.ids.vals.parent.scroll_to(self.ids.vals.children[0])
[docs]class NewDataLayout(BoxLayout): '''A layout handling the behavior of ``NewDataColumn`` and some inputs for each new value in :ref:`data`. .. versionadded:: 0.1.3 ''' def __init__(self, **kw): self.app = App.get_running_app() super(NewDataLayout, self).__init__(**kw)
[docs]class CreateWizard(Popup): '''A popup handling the behavior for creating a new :ref:`data`, i.e a wizard. .. versionadded:: 0.1.3 ''' run = ObjectProperty(None) def __init__(self, **kw): super(CreateWizard, self).__init__(**kw) self.run = kw.get('run') wdg = kw.get('wdg') if wdg: self.ids.container.add_widget(wdg)
[docs]class Dialog(Popup): '''A dialog handling the behavior for creating or opening files e.g. :ref:`project` or :ref:`data`. .. versionadded:: 0.1.0 ''' confirm = StringProperty('') run = ObjectProperty(None) dirs = BooleanProperty(False) project = BooleanProperty(False) filter = ListProperty([]) def __init__(self, **kw): super(Dialog, self).__init__(**kw) self.confirm = kw.get('confirm', '') self.run = kw.get('run') self.dirs = kw.get('dirs', False) self.project = kw.get('project', False) if self.project: self.ids.name.hint_text = 'Project.krysa' self.filter = [lambda folder, fname: fname.endswith('.krysa')] else: self.ids.name.hint_text = 'example.sqlite' self.filter = [lambda folder, fname: fname.endswith('.sqlite')]
[docs]class SideItem(BoxLayout): '''Supposed to be a part of settings, most likely will be removed/replaced. .. versionadded:: 0.1.0 '''
[docs]class TableItem(TextInput): '''An item handling the behavior or each separate value in the :mod:`main.Table` such as updating/editing values in :ref:`data`. .. versionadded:: 0.1.0 ''' def __init__(self, **kwargs): super(TableItem, self).__init__(**kwargs) self.bind(focus=self.on_focus)
[docs] def on_focus(self, widget, focused): '''Makes sure the unconfirmed value is discarded e.g. when clicked outside of the widget. ''' if not focused: self.text = self.old_text
[docs] def update_value(self, txt, *args): '''On ``<enter>`` (``return``) key updates the values :mod:`main.TableItem.text` and :mod:`main.TableItem.old_text` in :mod:`main.Table`. .. versionadded:: 0.1.0 ''' data = [] cols = self.cols - 1 for i in self.origin.data: if 'cell' in i: data.append(i) chunks = [data[x:x + cols] for x in xrange(0, len(data), cols)] orig_type = type(chunks[self.r][self.c - 1]) place = self.cols * (self.r + 1) - (self.cols - self.c) self.origin.data[place]['text'] = txt self.origin.data[place]['old_text'] = txt self.origin.refresh_from_data()
[docs]class Table(ScrollView): '''A view handling the behavior of the inputs from :ref:`sqlite`. Separates the values from :ref:`sqlite` according to its :ref:`data`'s column types into three Python categories - `int`, `float` or `unicode` and assigns an alphabetic order for each column together with row number to each value. .. versionadded:: 0.1.0 ''' # use with ....add_widget(Table(max_cols=3, max_rows=3)) # Grid -> Scroll, grid as container - better for sizing and placing # set "address" for table pos in grid to item number_size = (30, 30) default_size = (100, 30) def __init__(self, **kw): app = App.get_running_app() self.rv = RecycleView(bar_width='10dp', scroll_type=['bars', 'content']) container = RecycleGridLayout(size_hint=(None, None)) container.viewclass = TableItem container.bind(minimum_size=container.setter('size')) self.max_rows = kw.get('max_rows', 1) self.rows = self.max_rows + 1 self.max_cols = kw.get('max_cols', 1) self.cols = self.max_cols + 1 self.values = kw.get('values', []) self.labels = kw.get('labels') self.types = kw.get('types') container.rows = self.rows container.cols = self.cols ltr = [' (' + letter + ')' for letter in self.get_letters()] self.rv.add_widget(container) if 'item_size' not in kw: item_size = self.default_size super(Table, self).__init__(**kw) for r in range(self.rows): for c in range(self.cols): if r == 0: if c == 0: self.rv.data.append({'text': u'', 'disabled': True, 'size': self.number_size, 'origin': self.rv}) else: self.rv.data.append( { 'text': self.labels[c - 1] + ltr[c - 1], '_text': self.labels[c - 1], 'disabled': True, 'cell': 'label' + str(c - 1), 'type': type(u''), 'size': self.default_size, 'origin': self.rv } ) else: if c == 0: self.rv.data.append({'text': unicode(r), 'disabled': True, 'size': self.number_size, 'origin': self.rv}) else: filter = app.root.simple_chars try: val = self.values.pop(0) text_type = type(val) filter_val = repr(text_type)[7:-2] if filter_val in ['int', 'float']: filter = filter_val except IndexError: print 'values < space' val = '.' if 'e+' in str(val) or 'e-' in str(val): val = '{0:.10f}'.format(val) if self.types: if self.types[c - 1] == 'INTEGER': filter = 'int' text_type = type(1) elif self.types[c - 1] == 'REAL': filter = 'float' text_type = type(1.1) self.rv.data.append( { 'text': unicode(val), 'old_text': unicode(val), 'disabled': False, 'cell': self.labels[c - 1] + str(r), 'r': r, 'rows': self.rows, 'c': c, 'cols': self.cols, 'type': text_type, 'size': self.default_size, 'input_filter': filter, 'origin': self.rv } ) if self.values: raise Exception('Not enough space for all values! \ Increase rows/cols!') self.add_widget(self.rv)
[docs] def get_letters(self): '''Gets a list of letters the same length as :ref:`data`'s columns. .. versionadded:: 0.1.0 ''' letters = [chr(letter + 65) for letter in range(26)] result = [] label = [] cols = range(self.cols + 1) for i in cols: if i != 0: while i: i, rem = divmod(i - 1, 26) label[:0] = letters[rem] result.append(''.join(label)) label = [] return result
[docs] def lock(self, disabled=True): '''docs .. versionadded:: 0.1.0 ''' for i in self.rv.data: if 'cell' in i and 'label' not in i['cell']: i['disabled'] = disabled self.rv.refresh_from_data()
[docs] def clean(self, *args): '''Removes all data from :mod:`main.Table` .. versionadded:: 0.1.0 ''' self.rv.data = []
[docs]class ProcessFlow(BoxLayout, StencilView): '''A canvas on which will be displayed actions for each :ref:`data` related to them, such as used tasks connected with result of the tasks. .. versionadded:: 0.1.0 (Not implemented yet) ''' def __init__(self, **kw): super(ProcessFlow, self).__init__(**kw) app = App.get_running_app() self.texture = Image(source=app.path + '/data/grid.png').texture self.texture.wrap = 'repeat'
[docs]class SizedButton(Button): '''A button with width automatically customized according to text length of its siblings, which makes every sibling the same size as the one with the longest text string. .. versionadded:: 0.1.0 ''' def correct_width(self, *args): self.width = self.texture_size[0] + 8 self.parent.parent.width = max([c.width for c in self.parent.children]) for child in self.parent.children: child.width = self.parent.parent.width
[docs]class Body(FloatLayout): '''The main layout for the application. It handles menu values, their appropriate functions, filtering of user's input and functions for accessing :ref:`sqlite` in :mod:`main.Table`. .. versionadded:: 0.1.0 ''' def __init__(self, **kw): self.app = App.get_running_app() self.tables = [] self.app.menu = {'file': (['New...', self.new], ['Open Project', self.open_project], ['Close Project', self.close_project], ['Save Project', self.save_project], ['Import Data', self.import_data], ['Export Data', self.export_data], ['_Recent Projects', self.test], ['Exit', self.app.stop],), 'edit': (['_Undo', self.test], ['_Redo', self.test], ['_Cut', self.test], ['_Copy', self.test], ['_Paste', self.test], ['_Delete', self.test], ['_Find...', self.test], ['_Replace...', self.test], ['_Protect Data', self.test], ['_Go To...', self.test],), 'tasks': (['Basic', self.basic], ['_Averages', self.avgs], ['Manipulate', self.manipulate],), 'help': (['_KrySA Help', self.test], ['_Getting Started Tutorial', self.test], ['About KrySA', self.about],), } super(Body, self).__init__(**kw)
[docs] def new(self, button, *args): '''Opens a submenu for ``New`` menu. .. versionadded:: 0.1.0 ''' d = DropDown(allow_sides=True, auto_width=False) buttons = [] # Project saving works, but implementing ProcessFlow is still ToDo buttons.append(SizedButton(text='Project')) buttons[0].bind(on_release=self._new_project) buttons.append(SizedButton(text='Data')) buttons[1].bind(on_release=self._new_data) for b in buttons: d.add_widget(b) d.open(button)
[docs] def _new_project(self, *args): '''Closes already opened :ref:`project` if available and opens a dialog for creating a new one. .. versionadded:: 0.1.2 ''' self.close_project() self.savedlg = Dialog(title='New Project', confirm='Save', run=self._save_project, dirs=True, project=True) self.savedlg.open()
[docs] def _new_data(self, *args): '''Opens a wizard for creating a new :ref:`data` if a :ref:`project` is available or shows a warning if it doesn't exist. .. versionadded:: 0.1.3 ''' if not self.app.project_exists: error = ErrorPop(msg='No project exists!') error.open() return widget = NewDataLayout() self.wiz_newdata = CreateWizard(title='New Data', wdg=widget, run=partial(self._save_data, widget)) self.wiz_newdata.open()
[docs] def _save_data(self, wizard, *args): '''Gets data from the wizard, puts them into :mod:`main.Table` and exports them into :ref:`sqlite`. .. versionadded:: 0.1.4 ''' labels = [] types = [] values = [] table_name = wizard.ids.table_name.text if not len(table_name): error = ErrorPop(msg='Please name your data!') error.open() return if not len(wizard.ids.columns.children): error = ErrorPop(msg='There must be at least one column!') error.open() return for child in reversed(wizard.ids.columns.children): child.ids.checklock.dispatch('on_release') for child in reversed(wizard.ids.columns.children): if child.ids.colname.text not in labels: lbl = re.findall(r'([a-zA-Z0-9])', child.ids.colname.text) if not len(lbl): error = ErrorPop(msg='Use only a-z A-Z 0-9 characters!') error.open() return labels.append(''.join(lbl)) else: error = ErrorPop(msg='Each column must have a unique name!') error.open() return types.append(child.ids.coltype.text) column_values = [] for value_wdg in reversed(child.ids.vals.children): column_values.append(value_wdg.ids.value.text) values.append(column_values) max_cols = len(values) max_rows = len(max(values, key=len)) # create table in KrySA, then export tabletab = TabbedPanelItem(text=table_name) self.ids.tabpanel.add_widget(tabletab) _values = values[:] values = [] for i in range(max_rows): for j in range(max_cols): try: values.append(_values[j][i]) except IndexError: if types[j] == 'INTEGER': values.append(0) elif types[j] == 'REAL': values.append(0.0) else: values.append(u'') self.tables.append((table_name, Table(max_cols=max_cols, max_rows=max_rows, pos=self.pos, size=self.size, values=values, labels=labels, types=types))) tabletab.content = self.tables[-1][1] # export to data.sqlite in <project>/data directory data = op.join(self.app.project_dir, 'data') self._export_data([data], 'data.sqlite') self.wiz_newdata.dismiss()
def open_project(self, *args): self.close_project() self.opendlg = Dialog(title='Open Project', confirm='Open', run=self._open_project, project=True) self.opendlg.open()
[docs] def _open_project(self, selection, *args): '''Opens a :ref:`project` from path selected in ``Dialog`` and imports :ref:`sqlite`. .. versionadded:: 0.1.7 ''' if not selection: return else: selection, fname = op.split(selection[0]) data = op.join(selection, 'data') results = op.join(selection, 'results') self.app.project_exists = True self.app.project_dir = selection self.app.project_name = fname.split('.')[0] # (dummy for now) # dump widgets' properties from process flow to dict, then to json with open(op.join(selection, fname), 'rb') as f: project = json.loads(f.read()) print project # import from project automatically self._import_data([op.join(data, 'data.sqlite')]) # import results for file in os.listdir(results): self.set_page('', op.join(results, file), result_type='import')
[docs] def close_project(self, *args): '''Clears all important variables, removes all :ref:`data` available in :mod:`main.Table` and switches to :mod:`main.ProcessFlow`. .. versionadded:: 0.1.0 ''' # call this before a new project self.app.project_exists = False self.app.project_dir = '' self.app.project_name = '' tp = self.ids.tabpanel while len(tp.tab_list) > 1: self.tables.pop() tp.remove_widget(tp.tab_list[0]) self.ids.flow.clear_widgets() tp.switch_to(tp.tab_list[0])
def save_project(self, *args): # same as _new_project + export data & results # make some output if unsaved changes available(e.g. * before name) if self.app.project_exists: self._save_project() return self.savedlg = Dialog(title='New Project', confirm='Save', run=self._save_project, dirs=True, project=True) self.savedlg.open()
[docs] def _save_project(self, selection=None, fname=None, *args): '''Saves a :ref:`project` to path selected in ``Dialog`` and exports :ref:`sqlite`. .. versionadded:: 0.1.2 ''' if not selection: if not self.app.project_exists: return selection = op.dirname(self.app.project_dir) fname = self.app.project_name else: selection = selection[0] if '.' not in fname: fname = fname + '.krysa' if selection != op.dirname(self.app.project_dir): try: os.mkdir(op.join(selection, fname.split('.')[0])) except OSError: error = ErrorPop(msg='Project folder already exists!') error.open() return selection = op.join(selection, fname.split('.')[0]) data = op.join(selection, 'data') results = op.join(selection, 'results') if selection != self.app.project_dir: os.mkdir(data) os.mkdir(results) if op.exists(op.join(selection, fname)): os.remove(op.join(selection, fname)) self.app.project_exists = True self.app.project_dir = selection self.app.project_name = fname.split('.')[0] # (dummy for now) # dump widgets' properties from process flow to dict, then to json project = {u'test': u'blah'} with open(op.join(selection, fname), 'wb') as f: f.write(json.dumps(project, indent=4)) # let user set table columns, add to tab, then: self._export_data([data], 'data.sqlite') # exporting results to pdf -> 0.3.x self._export_results(results)
def import_data(self, *args): self.opendlg = Dialog(title='Import Data', confirm='Import', run=self._import_data) self.opendlg.open()
[docs] def _import_data(self, selection, *args): '''Imports :ref:`sqlite` from path selected in ``Dialog`` and puts it to :mod:`main.Table`. .. versionadded:: 0.1.0 ''' # limit table name and column name to [a-zA-Z] # CREATE TABLE test( # Column INTEGER NOT NULL CHECK( # typeof(Column) = 'integer')) if not selection: return else: selection = selection[0] if '.sqlite' not in selection: return conn = sqlite3.connect(op.join(selection)) c = conn.cursor() # get tables first! c.execute("SELECT name FROM sqlite_master WHERE type='table'") tables = [tab[0] for tab in c.fetchall()] for table in tables: c.execute("pragma table_info({0})".format(table)) table_info = c.fetchall() labels = [lbl[1] for lbl in table_info] # allow only: INTEGER, REAL, TEXT try: types = [type[2][0] for type in table_info] except IndexError: error = ErrorPop(msg='Bad file: No defined types in columns!') error.open() return tabletab = TabbedPanelItem(text=table) self.ids.tabpanel.add_widget(tabletab) c.execute('select * from {0}'.format(table)) values = [item for item in c.fetchone()] max_cols = len(values) values += [item for sublist in c.fetchall() for item in sublist] max_rows = int(math.ceil(len(values) / float(max_cols))) self.tables.append((table, Table(max_cols=max_cols, max_rows=max_rows, pos=self.pos, size=self.size, values=values, labels=labels))) tabletab.content = self.tables[-1][1] self.opendlg.dismiss() conn.close()
# ToDo: implement automatic saving to Project's data.sqlite # ToDo: if the same table name exist in tables, add ' (2)' or something def export_data(self, *args): self.savedlg = Dialog(title='Export Data', confirm='Export', run=self._export_data, dirs=True) self.savedlg.open() @staticmethod
[docs] def _extract_rows(data): '''Extract values from :mod:`main.Table`'s dictionary into a flat list. Example: ===== ===== ===== Data1 Data2 Data3 1 2.0 3 ===== ===== ===== [u'Data1', u'Data2', u'Data3', u'1', 2.0, 3, ...] .. versionadded:: 0.1.0 ''' rows = [] for item in data: try: item['cell'] if issubclass(item['type'], float): rows.append(float(item['text'])) elif issubclass(item['type'], int): rows.append(int(item['text'])) else: rows.append(item['text']) except KeyError: pass # protect against '' values -> int(''), float('') except ValueError: # duplicated zeros to prevent sqlite3.IntegrityError # - raised when there is type checking for inserting # values to the table e.g. type(0) != type(0.0) if issubclass(item['type'], float): rows.append(0.0) elif issubclass(item['type'], int): rows.append(0) data = [] return rows
[docs] def _export_data(self, selection, fname, *args): '''Exports all available :ref:`data` (visible as tabs) as :ref:`sqlite` into path selected in ``Dialog``. .. versionadded:: 0.1.1 ''' col_types = {"<type 'int'>": 'INTEGER', "<type 'float'>": 'REAL'} if not selection: return else: selection = selection[0] if op.exists(op.join(selection, fname)): os.remove(op.join(selection, fname)) conn = sqlite3.connect(op.join(selection, fname)) c = conn.cursor() for table in self.tables: col_string = '' max_cols = table[1].max_cols columns = table[1].rv.data[1:max_cols + 1] # types need whole dict types = table[1].rv.data[max_cols + 2:2 * (max_cols + 1)] for i, col in enumerate(columns): try: type = col_types[repr(types[i]['type'])] except KeyError: type = 'TEXT' # force column to check inserted values col_string += ( col['_text'] + " " + type + " NOT NULL CHECK(typeof(" + col['_text'] + ") = '" + type.lower() + "')," ) if col_string.endswith(','): col_string = col_string[:-1] c.execute( "CREATE TABLE " + table[0] + "(" + col_string + ")" ) rows = self._extract_rows(table[1].rv.data)[max_cols:] cnks = [] for x in xrange(0, len(rows), max_cols): cnks.append(rows[x:x + max_cols]) for chunk in cnks: _chunk = [] for cnk in chunk: if not isinstance(cnk, (str, unicode)): _chunk.append(unicode(cnk)) else: _chunk.append('\'' + cnk + '\'') values = ','.join(_chunk) c.execute( "INSERT INTO " + table[0] + " VALUES(" + values + ")" ) conn.commit() conn.close() try: self.savedlg.dismiss() except AttributeError: # either closed, or wasn't even opened pass
def _export_results(self, selection, *args): # 0.3.0 if not selection: return for file in os.listdir(selection): os.remove(op.join(selection, file)) result_wdg = self.ids.results for i, result in enumerate(reversed(self.ids.results.children)): try: where = op.join(selection, str(i).zfill(3) + '.png') result.children[2].children[0].export_to_png(where) except IndexError: print 'No results available.' # menu showing functions @staticmethod def basic(button, *args): drop = DropDown(allow_sides=True, auto_width=False) for t in Basic.names: but = SizedButton(text=t[0]) but.bind(on_release=t[1]) drop.add_widget(but) drop.open(button) @staticmethod def avgs(button, *args): drop = DropDown(allow_sides=True, auto_width=False) for t in Avgs.names: but = SizedButton(text=t[0]) but.bind(on_release=t[1]) drop.add_widget(but) drop.open(button) @staticmethod def manipulate(button, *args): drop = DropDown(allow_sides=True, auto_width=False) for t in Manipulate.names: but = SizedButton(text=t[0]) but.bind(on_release=t[1]) drop.add_widget(but) drop.open(button) @staticmethod
[docs] def about(*args): '''Displays `about` page of the app and includes other credits. .. versionadded:: 0.1.0 ''' aboutdlg = Popup(title='About') text = ( 'Copyright (C) 2016, KeyWeeUsr(Peter Badida)\n' 'License: GNU GPL v3.0\n' 'Find me @ https://github.com/KeyWeeUsr\n\n' 'Used Software:\n\n' 'Python\n' 'Copyright (C) 2001-2016 Python Software Foundation\n' 'All rights reserved.\n\n' 'Kivy\n' 'Copyright (C) 2010-2016 Kivy Team and other contributors\n\n' 'NumPy\n' 'Copyright (C) 2005-2016, NumPy Developers.\n' 'All rights reserved.\n\n' 'SciPy\n' 'Copyright (C) 2003-2013 SciPy Developers.\n' 'All rights reserved.\n\n' 'MatPlotLib\n' 'Copyright (C) 2002-2016 John D. Hunter\n' 'All Rights Reserved\n' ) aboutdlg.content = Label(text=text) aboutdlg.open()
@staticmethod def test(*args): print 'ping: ', args # non-menu related functions @staticmethod def get_column(address): col = 0 for c in address: if c in string.ascii_letters: col = col * 26 + (ord(c.upper()) - ord('A')) + 1 return col
[docs] def from_address(self, table, address, extended=False, *args): '''Gets value(s) from :mod:`main.Table` according to the address such as ``A1`` or ``A1:B2``. Values are fetched in the way that the final list contains even empty (``u''``) values. It is not expected of user to use :ref:`task` for strings and most of them won't even run. To get non-empty values for a :ref:`task` use for example Python's ``filter()``:: values = filter(lambda x: len(str(x)), values) This `filter`, however, will remain values such as ``None`` untouched. .. versionadded:: 0.1.0 .. versionchanged:: 0.3.5 Added extended options and a possibility to get ``:all`` values from data. ''' values = [] col_row = [] # [column, row] such as [x, y] |_ if ':' not in address: match = re.findall(r'([a-zA-Z]+)([0-9]+)', address) col_row.append([self.get_column(match[0][0]), match[0][1]]) else: if 'all' in address: start_col = 1 start_row = 1 end_col = self.tables[table][1].cols end_row = self.tables[table][1].rows else: addresses = address.split(':') match = re.findall(r'([a-zA-Z]+)([0-9]+)', ' '.join(addresses)) start_col = self.get_column(match[0][0]) start_row = int(match[0][1]) end_col = self.get_column(match[1][0]) end_row = int(match[1][1]) for col in range(start_col - 1, end_col): for row in range(start_row - 1, end_row): col_row.append([col + 1, row + 1]) col_range = 0 row_range = 0 val_place = [None, None] # [column, row] table_cols = self.tables[table][1].cols table_rows = self.tables[table][1].rows labels = self.tables[table][1].labels while col_row: c, r = col_row.pop(0) c = int(c) r = int(r) if 0 < r < table_rows and 0 < c < table_cols: # increment ranges if extended: if val_place[0] != c: val_place[0] = c col_range += 1 if val_place[1] != r: val_place[1] = r row_range += 1 # get values key = table_cols * (r + 1) - (table_cols - c) item = self.tables[table][1].rv.data[key] if item['r'] == r and item['c'] == c: if issubclass(item['type'], float): values.append(float(item['text'])) elif issubclass(item['type'], int): values.append(int(item['text'])) else: values.append(item['text']) if extended: if row_range < col_range: # return 1 manually if only one row # otherwise row_range == 0 row_range = 1 else: row_range = row_range / int(col_range) return values, col_range, row_range, labels else: return values
[docs] def set_page(self, task, result, result_type='text', footer='time'): '''Creates a :mod:`main.PageBox` for a result. The header consists of the :ref:`task`'s name, the footer is by default the time when the result was created and the content depends on `result_type` which can be - text, image(path to image) or widget. If `result_type == 'widget'`, result has to be an instance of a widget (obviously containing the output), e.g.:: b = Button(text='my output') set_page('MyTask', b, result_type='widget') .. note:: When exporting pages, everything is converted into images (pngs), therefore making fancy behaving widgets is irrelevant. .. versionadded:: 0.2.0 .. versionchanged:: 0.3.2 Added tables as a result type. ''' page = PageBox() head = PaperLabel(text=task, size_hint_y=None, height='30dp') if result_type == 'text': content = PaperLabel(text=result) elif result_type in ['image', 'import']: content = Image(source=result) elif 'table' in result_type: # result == list of values box = BoxLayout(orientation='vertical') grid = ResultGrid(cols=int(result_type.strip('table')), padding=1, spacing=1, size_hint=[0.95, None], pos_hint={'center_x': 0.5}) grid.bind(minimum_height=grid.setter('height')) for value in result: grid.add_widget(Wrap(text=' ' + str(value), color=[0, 0, 0, 1], background_color=[1, 1, 1, 1])) box.add_widget(Widget()) box.add_widget(grid) box.add_widget(Widget()) content = box elif result_type == 'widget': content = result # turn off with footer=None if result_type == 'import': pass elif not footer: foot = PaperLabel(size_hint_y=None, height='30dp') else: if footer == 'time': t = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) foot = PaperLabel(text=t, size_hint_y=None, height='30dp') else: foot = PaperLabel(text=footer, size_hint_y=None, height='30dp') if not result_type == 'import': page.add_widget(head) page.add_widget(content) page.add_widget(foot) else: page.add_widget(content) self.ids.results.add_widget(page, 1) panel = self.ids.resultspanel if panel.width == panel.min_size: panel.width = panel.max_size
@staticmethod def simple_chars(substring, from_undo): chars = re.findall(r'([a-zA-Z0-9.])', substring) return u''.join(chars) @staticmethod def address_chars(substring, from_undo): chars = re.findall(r'([a-zA-Z0-9:])', substring) return u''.join(chars) def update_tree(self, dt): tree = self.ids.tree lab = TreeViewLabel for node in tree.iterate_all_nodes(): tree.remove_node(node) root = tree.add_node(lab(text=self.app.project_name, is_open=True)) for path, folders, files in os.walk(self.app.project_dir): if op.basename(self.app.project_dir) != op.basename(path): dirname = op.basename(path).title() parent = tree.add_node(lab(text=dirname, is_open=True), root) for file in files: filename = op.basename(file) tree.add_node(lab(text=filename, is_open=True), parent)
[docs]class KrySA(App): '''The main class of the application through which is handled the communication of other classes with getting an instance of the app via ``App.get_running_app()``. Other than that, it holds important variables of :ref:`project`, sql blacklist for :ref:`sqlite` creating and updating or the application properties themselves. ''' path = op.dirname(op.abspath(__file__)) icon = path + '/data/icon.png' project_exists = BooleanProperty(False) project_name = '' project_dir = '' title = 'KrySA' errorcls = ErrorPop tablecls = Table sql_blacklist = ['DROP', 'EXEC', 'DECLARE', 'UPDATE', 'CREATE', 'DELETE', 'INSERT', 'JOIN', '=', '"', "'", ';']
[docs] def on_project_exists(self, instance, exists): '''Checks change of :mod:`main.KrySA.project_exists` and if :ref:`project` exists, schedules updating of its tree to 5 second interval. .. versionadded:: 0.3.0 ''' if exists: self.treeclock = Clock.schedule_interval(self.root.update_tree, 5) else: Clock.unschedule(self.treeclock)
[docs] def build(self): '''Default Kivy function for getting the root widget of application. ''' # read MenuDrop if an idea of removing it comes up MenuDrop() return Body()
if __name__ == '__main__': KrySA().run()