class MDSpinner(ThemableBehavior, Widget): """:class:`MDSpinner` is an implementation of the circular progress indicator in Google's Material Design. It can be used either as an indeterminate indicator that loops while the user waits for something to happen, or as a determinate indicator. Set :attr:`determinate` to **True** to activate determinate mode, and :attr:`determinate_time` to set the duration of the animation. """ determinate = BooleanProperty(False) """:attr:`determinate` is a :class:`~kivy.properties.BooleanProperty` and defaults to False """ determinate_time = NumericProperty(2) """:attr:`determinate_time` is a :class:`~kivy.properties.NumericProperty` and defaults to 2 """ active = BooleanProperty(True) """Use :attr:`active` to start or stop the spinner. :attr:`active` is a :class:`~kivy.properties.BooleanProperty` and defaults to True """ color = ListProperty([]) """:attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to 'self.theme_cls.primary_color' """ _alpha = NumericProperty(0) _rotation_angle = NumericProperty(360) _angle_start = NumericProperty(0) _angle_end = NumericProperty(8) def __init__(self, **kwargs): super(MDSpinner, self).__init__(**kwargs) self.color = self.theme_cls.primary_color self._alpha_anim_in = Animation(_alpha=1, duration=.8, t='out_quad') self._alpha_anim_out = Animation(_alpha=0, duration=.3, t='out_quad') self._alpha_anim_out.bind(on_complete=self._reset) self.theme_cls.bind(primary_color=self._update_color) if self.determinate: self._start_determinate() else: self._start_loop() def _update_color(self, *args): self.color = self.theme_cls.primary_color def _start_determinate(self, *args): self._alpha_anim_in.start(self) _rot_anim = Animation(_rotation_angle=0, duration=self.determinate_time * .7, t='out_quad') _rot_anim.start(self) _angle_start_anim = Animation(_angle_end=360, duration=self.determinate_time, t='in_out_quad') _angle_start_anim.bind(on_complete=lambda *x: \ self._alpha_anim_out.start(self)) _angle_start_anim.start(self) def _start_loop(self, *args): if self._alpha == 0: _rot_anim = Animation(_rotation_angle=0, duration=2, t='linear') _rot_anim.start(self) self._alpha = 1 self._alpha_anim_in.start(self) _angle_start_anim = Animation(_angle_end=self._angle_end + 270, duration=.6, t='in_out_cubic') _angle_start_anim.bind(on_complete=self._anim_back) _angle_start_anim.start(self) def _anim_back(self, *args): _angle_back_anim = Animation(_angle_start=self._angle_end - 8, duration=.6, t='in_out_cubic') _angle_back_anim.bind(on_complete=self._start_loop) _angle_back_anim.start(self) def on__rotation_angle(self, *args): if self._rotation_angle == 0: self._rotation_angle = 360 if not self.determinate: _rot_anim = Animation(_rotation_angle=0, duration=2) _rot_anim.start(self) def _reset(self, *args): Animation.cancel_all(self, '_angle_start', '_rotation_angle', '_angle_end', '_alpha') self._angle_start = 0 self._angle_end = 8 self._rotation_angle = 360 self._alpha = 0 self.active = False def on_active(self, *args): if not self.active: self._reset() else: if self.determinate: self._start_determinate() else: self._start_loop()
class IRCController(EventDispatcher): server = "irc.quakenet.org" port = 6667 is_connected = BooleanProperty(False) def on_is_connected(self, instance, value): maingui = App.get_running_app().root irc_ids = [id for id in maingui.ids if id.startswith("irc")] if value: # enable button for id in irc_ids: maingui.ids[id].on_connected() maingui.ids.btn_connectIRC.text = "Disconnect from IRC" else: # disable button and input for id in irc_ids: maingui.ids[id].on_disconnected() maingui.ids.btn_connectIRC.text = "Connect to IRC" def connect(self): if self.is_connected: return self.ircfactory = IRCFactory(self) self.connector = reactor.connectTCP(IRCController.server, IRCController.port, self.ircfactory) def disconnect(self): if not self.is_connected: return self.ircfactory.client.quit() self.connector.disconnect() self.is_connected = False for channel in self.ircfactory.client.joined_channels: self.get_irc_widget(channel).ids.btn_join_part.text = "Join" self.ircfactory = None def toggle_connection(self): """ Toggle the connection to the Quakenet IRC servers """ if not self.is_connected: self.connect() app = App.get_running_app() app.root.ids.btn_connectIRC.text = "Connecting to IRC" else: self.disconnect() def join_or_part(self, channel): if not self.is_connected: return if channel in self.ircfactory.client.joined_channels: self.ircfactory.client.leave(channel) else: self.ircfactory.client.join(channel) def get_irc_widget(self, channel): maingui = App.get_running_app().root irc_ids = [id for id in maingui.ids if id.startswith("irc")] for id in irc_ids: if maingui.ids[id].channel == channel: return maingui.ids[id]
class UserSelector(Button): """ Show and select connected users """ is_active = BooleanProperty(False) users_dropdown = ObjectProperty(None) usernames = ListProperty() selected_username = StringProperty() _connection = ObjectProperty(None) _update_event = ObjectProperty(None) _updating_users = BooleanProperty(False) _new_user_selected = BooleanProperty(False) _selecting_user = BooleanProperty(False) def __init__(self, **kwargs): super().__init__(**kwargs) self._app = App.get_running_app() self.users_dropdown = DropDown(on_select=self._select_user) print("constructor called") def _select_user(self, _, username): """ Select a user :param username: The username to select """ self.selected_username = username self._new_user_selected = True Logger.info(f"User selector:Selected partner: {username}") def _check_for_usernames_response(self): return self._connection.socket.recv(block=False) @staticmethod def _handle_usernames_response(response): """ parse the usernames from the server response :param response: The response the server sent :return: A list of the usernames the server sent """ usernames = response.get_content_as_text().split(", ") Logger.debug(f"User selector:Received usernames: {usernames}") return usernames def _update_dropdown(self, usernames): """ Update the list of the usernames the dropdown displays :param usernames: The usernames to display """ self.users_dropdown.clear_widgets() for username in usernames: self.users_dropdown.add_widget( Button(size_hint_y=None, size=self.size, text=username, on_release=lambda button: self.users_dropdown.select( button.text))) def _check_for_select_response(self): return self._connection.socket.recv(block=False) def _handle_select_response(self, response): pass def _finnish_selecting(self): self._app.partner = self.selected_username def _send_usernames_request(self): Logger.debug("User selector:Requesting usernames") self._connection.socket.send( Message(MESSAGE_TYPES["server interaction"], "get all connected usernames")) def _send_select_request(self): Logger.debug( f"User selector:Sending set partner: {self.selected_username}") self._connection.socket.send( Message(MESSAGE_TYPES["server interaction"], f"set partner\n{self.selected_username}")) def _update_users(self, _): try: if self._updating_users: usernames_response = self._check_for_usernames_response() if usernames_response is not None: usernames = UserSelector._handle_usernames_response( usernames_response) self._update_dropdown(usernames) self._updating_users = False elif self._selecting_user: select_response = self._check_for_select_response() if select_response is not None: self._handle_select_response(select_response) self._finnish_selecting() self._selecting_user = False elif self._new_user_selected: self._send_select_request() self._selecting_user = True self._new_user_selected = False else: self._send_usernames_request() self._updating_users = True except ConnectionClosed: # Unexpected close Logger.error("User selector:Unexpected close, closing!") self.close() except Exception as e: # TODO: be more specific print(e) Logger.error("User selector:Unexpected error, closing!", exc_info=True) self.close() def on_release(self): """ Open the dropdown """ Logger.debug("User selector:Attempting to open dropdown") if self.is_active: Logger.debug("User selector:Opening dropdown") self.users_dropdown.open(self) else: Logger.debug("User selector:Did not open dropdown " "since it is closed or had not started") def start(self, connection): """ Start receiving users and displaying them :param connection: The connection to get the users with """ Logger.info("User selector:Starting") self.is_active = True self._connection = connection self._update_event = Clock.schedule_interval( self._update_users, UPDATE_USERS_REFRESH_RATE) def close(self): """ Close the users getter Only call this after opening """ Logger.info("User selector:Closing") self.is_active = False if self._update_event is not None: self._update_event.cancel()
class MDFileManager(ThemableBehavior, MDRelativeLayout): icon = StringProperty("check") """ The icon that will be used on the directory selection button. :attr:`icon` is an :class:`~kivy.properties.StringProperty` and defaults to `check`. """ icon_folder = StringProperty(f"{images_path}folder.png") """ The icon that will be used for folder icons when using ``preview = True``. :attr:`icon` is an :class:`~kivy.properties.StringProperty` and defaults to `check`. """ exit_manager = ObjectProperty(lambda x: None) """ Function called when the user reaches directory tree root. :attr:`exit_manager` is an :class:`~kivy.properties.ObjectProperty` and defaults to `lambda x: None`. """ select_path = ObjectProperty(lambda x: None) """ Function, called when selecting a file/directory. :attr:`select_path` is an :class:`~kivy.properties.ObjectProperty` and defaults to `lambda x: None`. """ ext = ListProperty() """ List of file extensions to be displayed in the manager. For example, `['.py', '.kv']` - will filter out all files, except python scripts and Kv Language. :attr:`ext` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ search = OptionProperty("all", options=["all", "dirs", "files"]) """ It can take the values 'all' 'dirs' 'files' - display only directories or only files or both them. By default, it displays folders, and files. Available options are: `'all'`, `'dirs'`, `'files'`. :attr:`search` is an :class:`~kivy.properties.OptionProperty` and defaults to `all`. """ current_path = StringProperty(os.getcwd()) """ Current directory. :attr:`current_path` is an :class:`~kivy.properties.StringProperty` and defaults to `/`. """ use_access = BooleanProperty(True) """ Show access to files and directories. :attr:`use_access` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ preview = BooleanProperty(False) """ Shows only image previews. :attr:`preview` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ show_hidden_files = BooleanProperty(False) """ Shows hidden files. :attr:`show_hidden_files` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ sort_by = OptionProperty( "name", options=["nothing", "name", "date", "size", "type"]) """ It can take the values 'nothing' 'name' 'date' 'size' 'type' - sorts files by option By default, sort by name. Available options are: `'nothing'`, `'name'`, `'date'`, `'size'`, `'type'`. :attr:`sort_by` is an :class:`~kivy.properties.OptionProperty` and defaults to `name`. """ sort_by_desc = BooleanProperty(False) """ Sort by descending. :attr:`sort_by_desc` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ selector = OptionProperty("any", options=["any", "file", "folder", "multi"]) """ It can take the values 'any' 'file' 'folder' 'multi' By default, any. Available options are: `'any'`, `'file'`, `'folder'`, `'multi'`. :attr:`selector` is an :class:`~kivy.properties.OptionProperty` and defaults to `any`. """ selection = ListProperty() """ Contains the list of files that are currently selected. :attr:`selection` is a read-only :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ _window_manager = None _window_manager_open = False def __init__(self, **kwargs): super().__init__(**kwargs) toolbar_label = self.ids.toolbar.children[1].children[0] toolbar_label.font_style = "Subtitle1" if (self.selector == "any" or self.selector == "multi" or self.selector == "folder"): self.add_widget( FloatButton( callback=self.select_directory_on_press_button, md_bg_color=self.theme_cls.primary_color, icon=self.icon, )) if self.preview: self.ext = [".png", ".jpg", ".jpeg"] self.disks = [] def show_disks(self) -> None: if platform == "win": self.disks = sorted( re.findall( r"[A-Z]+:.*$", os.popen("mountvol /").read(), re.MULTILINE, )) elif platform in ["linux", "android"]: self.disks = sorted( re.findall( r"on\s(/.*)\stype", os.popen("mount").read(), )) elif platform == "macosx": self.disks = sorted( re.findall( r"on\s(/.*)\s\(", os.popen("mount").read(), )) else: return self.current_path = "" manager_list = [] for disk in self.disks: access_string = self.get_access_string(disk) if "r" not in access_string: icon = "harddisk-remove" else: icon = "harddisk" manager_list.append({ "viewclass": "BodyManager", "path": disk, "icon": icon, "dir_or_file_name": disk, "events_callback": self.select_dir_or_file, "_selected": False, }) self.ids.rv.data = manager_list if not self._window_manager: self._window_manager = ModalView(size_hint=self.size_hint, auto_dismiss=False) self._window_manager.add_widget(self) if not self._window_manager_open: self._window_manager.open() self._window_manager_open = True def show(self, path: str) -> None: """ Forms the body of a directory tree. :param path: The path to the directory that will be opened in the file manager. """ self.current_path = path self.selection = [] dirs, files = self.get_content() manager_list = [] if dirs == [] and files == []: # selected directory pass elif not dirs and not files: # directory is unavailable return if self.preview: for name_dir in self.__sort_files(dirs): manager_list.append({ "viewclass": "BodyManagerWithPreview", "path": self.icon_folder, "realpath": os.path.join(path), "type": "folder", "name": name_dir, "events_callback": self.select_dir_or_file, "height": dp(150), "_selected": False, }) for name_file in self.__sort_files(files): if (os.path.splitext(os.path.join(path, name_file))[1] in self.ext): manager_list.append({ "viewclass": "BodyManagerWithPreview", "path": os.path.join(path, name_file), "name": name_file, "type": "files", "events_callback": self.select_dir_or_file, "height": dp(150), "_selected": False, }) else: for name in self.__sort_files(dirs): _path = os.path.join(path, name) access_string = self.get_access_string(_path) if "r" not in access_string: icon = "folder-lock" else: icon = "folder" manager_list.append({ "viewclass": "BodyManager", "path": _path, "icon": icon, "dir_or_file_name": name, "events_callback": self.select_dir_or_file, "_selected": False, }) for name in self.__sort_files(files): if self.ext and os.path.splitext(name)[1] not in self.ext: continue manager_list.append({ "viewclass": "BodyManager", "path": name, "icon": "file-outline", "dir_or_file_name": os.path.split(name)[1], "events_callback": self.select_dir_or_file, "_selected": False, }) self.ids.rv.data = manager_list if not self._window_manager: self._window_manager = ModalView(size_hint=self.size_hint, auto_dismiss=False) self._window_manager.add_widget(self) if not self._window_manager_open: self._window_manager.open() self._window_manager_open = True def get_access_string(self, path: str) -> str: access_string = "" if self.use_access: access_data = {"r": os.R_OK, "w": os.W_OK, "x": os.X_OK} for access in access_data.keys(): access_string += (access if os.access( path, access_data[access]) else "-") return access_string def get_content( self, ) -> Union[Tuple[List[str], List[str]], Tuple[None, None]]: """Returns a list of the type [[Folder List], [file list]].""" try: files = [] dirs = [] for content in os.listdir(self.current_path): if os.path.isdir(os.path.join(self.current_path, content)): if self.search == "all" or self.search == "dirs": if (not self.show_hidden_files) and ( content.startswith(".")): continue else: dirs.append(content) else: if self.search == "all" or self.search == "files": if len(self.ext) != 0: try: files.append( os.path.join(self.current_path, content)) except IndexError: pass else: if (not self.show_hidden_files and content.startswith(".")): continue else: files.append(content) return dirs, files except OSError: return None, None def close(self) -> None: """Closes the file manager window.""" self._window_manager.dismiss() self._window_manager_open = False def select_dir_or_file( self, path: str, widget: Union[BodyManagerWithPreview, Factory.BodyManager], ): """Called by tap on the name of the directory or file.""" if os.path.isfile(os.path.join(self.current_path, path)): if self.selector == "multi": file_path = os.path.join(self.current_path, path) if file_path in self.selection: widget._selected = False self.selection.remove(file_path) else: widget._selected = True self.selection.append(file_path) elif self.selector == "folder": return else: self.select_path(os.path.join(self.current_path, path)) else: self.current_path = path self.show(path) def back(self) -> None: """Returning to the branch down in the directory tree.""" path, end = os.path.split(self.current_path) if self.current_path and path == self.current_path: self.show_disks() else: if not end: self.close() self.exit_manager(1) else: self.show(path) def select_directory_on_press_button(self, *args) -> None: """Called when a click on a floating button.""" if self.selector == "multi": if len(self.selection) > 0: self.select_path(self.selection) else: if self.selector == "folder" or self.selector == "any": self.select_path(self.current_path) def __sort_files(self, files): def sort_by_name(files): files.sort(key=locale.strxfrm) files.sort(key=str.casefold) return files if self.sort_by == "name": sorted_files = sort_by_name(files) elif self.sort_by == "date": _files = sort_by_name(files) _sorted_files = [ os.path.join(self.current_path, f) for f in _files ] _sorted_files.sort(key=os.path.getmtime, reverse=True) sorted_files = [os.path.basename(f) for f in _sorted_files] elif self.sort_by == "size": _files = sort_by_name(files) _sorted_files = [ os.path.join(self.current_path, f) for f in _files ] _sorted_files.sort(key=os.path.getsize, reverse=True) sorted_files = [os.path.basename(f) for f in _sorted_files] elif self.sort_by == "type": _files = sort_by_name(files) sorted_files = sorted( _files, key=lambda f: (os.path.splitext(f)[1], os.path.splitext(f)[0]), ) else: sorted_files = files if self.sort_by_desc: sorted_files.reverse() return sorted_files
class Ponerine(BoxLayout): version = StringProperty(__version__) JsonData = {} token = NumericProperty() Jsoncounter = NumericProperty(0) Jsonflip = NumericProperty(0) connected = BooleanProperty() camconfig = {} def CamConnect(self): #try: self.camaddr = "192.168.42.1" self.camport = 7878 self.camdataport = 8787 self.camwebport = 80 self.token = 0 print self.camaddr, self.camport, self.camdataport, self.camwebport socket.setdefaulttimeout(5) self.srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #create socket self.srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) self.srv.connect((self.camaddr, self.camport)) #open socket self.thread_read = threading.Thread(target=self.JsonReader) self.thread_read.setDaemon(True) self.thread_read.setName('JsonReader') self.thread_read.start() waiter = 0 #self.token = "" while 1: if self.connected: print "self.token before empty", self.token if self.token == 0: print "self.token empty" break self.UpdateUsage() self.UpdateBattery() self.ReadConfig() self.SDOK = True if self.camconfig[ "sd_card_status"] == "insert" and self.totalspace > 0: if self.camconfig["sdcard_need_format"] != "no-need": if not self.ActionForceFormat(): self.SDOK = False print "SD memory card not formatted!" else: self.SDOK = False print "No SD memory card inserted in camera!" if self.SDOK == True: print "SD Usage: " print "Battery: " break else: if waiter <= 5: time.sleep(1) waiter += 1 else: raise Exception('Connection', 'failed') #throw an exception #except Exception as e: #self.connected = False #print "Connect Error:", "Cannot connect to the address specified" #self.srv.close() def JsonReader(self): self.JsonData = {} self.Jsoncounter = 0 self.Jsonflip = 0 initcounter = 0 self.srv.send('{"msg_id":257,"token":0}') #auth to the camera while initcounter < 300: self.JsonLoop() initcounter += 1 if len(self.JsonData) > 0: break if len(self.JsonData) > 0: self.srv.setblocking(0) self.connected = True while self.connected: self.JsonLoop() def JsonLoop(self): #try: ready = select.select([self.srv], [], []) if ready[0]: byte = self.srv.recv(1) if byte == "{": self.Jsoncounter += 1 self.Jsonflip = 1 elif byte == "}": self.Jsoncounter -= 1 self.JsonData += byte if self.Jsonflip == 1 and self.Jsoncounter == 0: # try: data_dec = json.loads(self.JsonData) print "one piece json:", data_dec self.JsonData = {} self.Jsonflip = 0 if "msg_id" in data_dec.keys(): print "msg_id:", data_dec["msg_id"] if data_dec["msg_id"] == 257: print "param:", data_dec["param"] self.token = data_dec["param"] print "Loop token:", self.token elif data_dec["msg_id"] == 7: if "type" in data_dec.keys( ) and "param" in data_dec.keys(): if data_dec["type"] == "battery": self.thread_Battery = threading.Thread( target=self.UpdateBattery) self.thread_Battery.setName('UpdateBattery') self.thread_Battery.start() elif data_dec["type"] == "start_photo_capture": if self.camconfig[ "capture_mode"] == "precise quality cont.": self.bphoto.config(text="Stop\nTIMELAPSE", bg="#ff6666") self.brecord.config(state=DISABLED) self.brecord.update_idletasks() self.bphoto.update_idletasks() self.thread_ReadConfig = threading.Thread( target=self.ReadConfig) self.thread_ReadConfig.setDaemon(True) self.thread_ReadConfig.setName( 'ReadConfig') self.thread_ReadConfig.start() elif data_dec["type"] == "precise_cont_complete": if self.camconfig[ "capture_mode"] == "precise quality cont.": self.bphoto.config(text="Start\nTIMELAPSE", bg="#66ff66") self.brecord.config(state="normal") self.brecord.update_idletasks() self.bphoto.update_idletasks() self.thread_ReadConfig = threading.Thread( target=self.ReadConfig) self.thread_ReadConfig.setDaemon(True) self.thread_ReadConfig.setName( 'ReadConfig') self.thread_ReadConfig.start() self.JsonData[data_dec["msg_id"]] = data_dec else: raise Exception('Unknown', 'data') # except Exception as e: # print data, e #except Exception: # self.connected = False def ReadConfig(self): tosend = '{"msg_id":3,"token":%s}' % self.token resp = self.Comm(tosend) self.camconfig = {} for each in resp["param"]: self.camconfig.update(each) def Comm(self, tosend): Jtosend = json.loads(tosend) msgid = Jtosend["msg_id"] self.JsonData[msgid] = "" self.srv.send(tosend) while self.JsonData[msgid] == "": continue #wrong token, ackquire new one & resend - "workaround" for camera insisting on tokens if self.JsonData[msgid]["rval"] == -4: self.token = "" self.srv.send('{"msg_id":257,"token":0}') while self.token == "": continue Jtosend["token"] = self.token tosend = json.dumps(Jtosend) self.JsonData[msgid] = "" self.srv.send(tosend) while self.JsonData[msgid] == "": continue return self.JsonData[msgid]
class ShinobiMonitor(BoxLayout): serverURL = StringProperty() monitorPath = StringProperty() loading = BooleanProperty(False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._data = {} self._state = 'stop' self._update_event = None self.image = Image(allow_stretch=True, keep_ratio=False) self.loading_image = Image(source='data/images/image-loading.gif', allow_stretch=False, keep_ratio=True) def on_loading(self, instnace, value): if value: self.remove_widget(self.image) self.add_widget(self.loading_image) else: self.add_widget(self.image) self.remove_widget(self.loading_image) @property def snapshot_url(self): return '%s%s' % (self.serverURL.rstrip('/'), self._data['snapshot']) @property def state(self): return self._state def start(self): self.loading = True self._state = 'play' self._image_lock = threading.Lock() self._image_buffer = None self.image_thread = threading.Thread(target=self._fetch_image, daemon=True) self.meta_thread = threading.Thread(target=self._fetch_metadata, daemon=True) self.meta_thread.start() def stop(self): self._state = 'stop' self.loading = False Clock.unschedule(self._update_event) def _fetch_metadata(self): while self.state == 'play': try: response = requests.get( '%s/%s' % (self.serverURL.rstrip('/'), self.monitorPath.strip('/')), timeout=10) except: time.sleep(1) continue if response.status_code == 200: self._data = response.json() self.image_thread.start() self._update_event = Clock.schedule_interval( self._update_image, 1) break def _fetch_image(self): while self.state == 'play': try: response = requests.get(self.snapshot_url, timeout=0.5) except: time.sleep(0.5) self.loading = True continue if response.status_code == 200: data = BytesIO(response.content) im = CoreImage(data, ext="jpeg", nocache=True) with self._image_lock: self._image_buffer = im if self.loading: self.loading = False time.sleep(1) def _update_image(self, *args): im = None with self._image_lock: im = self._image_buffer self._image_buffer = None if im is not None: self.image.texture = im.texture self.image.texture_size = im.texture.size
class ReceiveScreen(Screen): connect_button_disabled = BooleanProperty(False) connect_button_text = StringProperty('connect') accept_button_disabled = BooleanProperty(True) accept_button_func = ObjectProperty(None) accept_button_text = StringProperty('waiting for offer') file_name = StringProperty('…') file_size = StringProperty('…') def on_pre_enter(self): """ Called just before the user enters this screen. """ self.downloads_dir = get_downloads_dir() self.connect_button_disabled = False self.connect_button_text = 'connect' self.accept_button_disabled = True self.accept_button_func = self.accept_offer self.accept_button_text = 'waiting for offer' self.file_name = '…' self.file_size = '…' self.ids.code_input.text = '' def open_wormhole(self): """ Called when the user releases the connect button. """ code = self.ids.code_input.text.strip() code = '-'.join(code.split()) if not code: return ErrorPopup.show('Please enter a code.') def connect(): self.connect_button_disabled = True self.connect_button_text = 'connecting' self.wormhole = Wormhole() deferred = self.wormhole.connect(code) deferred.addCallbacks(exchange_keys, ErrorPopup.show) def exchange_keys(code): self.connect_button_disabled = True self.connect_button_text = 'exchanging keys' deferred = self.wormhole.exchange_keys() deferred.addCallbacks(await_offer, ErrorPopup.show) def await_offer(verifier): self.connect_button_disabled = True self.connect_button_text = 'connected' deferred = self.wormhole.await_offer() deferred.addCallbacks(show_offer, ErrorPopup.show) def show_offer(offer): self.file_name = str(offer['filename']) self.file_size = humanize.naturalsize(offer['filesize']) self.accept_button_disabled = False self.accept_button_text = 'accept' connect() def accept_offer(self): """ Called when the user releases the accept button. """ file_path = os.path.join(self.downloads_dir, self.file_name) def show_error(): ErrorPopup.show( ('You cannot receive a file if the app cannot write it.')) @ensure_storage_perms(show_error) def accept_offer(): self.accept_button_disabled = True self.accept_button_text = 'receiving' deferred = self.wormhole.accept_offer(file_path) deferred.addCallbacks(show_done, ErrorPopup.show) def show_done(hex_digest): self.accept_button_disabled = False self.accept_button_func = self.open_file self.accept_button_text = 'open file' accept_offer() def open_file(self): """ Called when the user presses the accept button after the file transfer has been completed. """ file_path = os.path.join(self.downloads_dir, self.file_name) def show_error(): ErrorPopup.show('Cannot open {}'.format(file_path)) @ensure_storage_perms(show_error) def do(): open_file(file_path) if os.path.exists(file_path): do() else: show_error() def on_leave(self): """ Called when the user leaves this screen. """ try: self.wormhole.close() except AttributeError: pass
class ActionGroup(ActionItem, Spinner): '''ActionGroup class, see module documentation for more information. ''' use_separator = BooleanProperty(False) '''Specifies whether to use a separator after/before this group or not. :attr:`use_separator` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' separator_image = StringProperty( 'atlas://data/images/defaulttheme/separator') '''Background Image for an ActionSeparator in an ActionView. :attr:`separator_image` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/separator'. ''' separator_width = NumericProperty(0) '''Width of the ActionSeparator in an ActionView. :attr:`separator_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' mode = OptionProperty('normal', options=('normal', 'spinner')) '''Sets the current mode of an ActionGroup. If mode is 'normal', the ActionGroups children will be displayed normally if there is enough space, otherwise they will be displayed in a spinner. If mode is 'spinner', then the children will always be displayed in a spinner. :attr:`mode` is a :class:`~kivy.properties.OptionProperty` and defaults to 'normal'. ''' dropdown_width = NumericProperty(0) '''If non zero, provides the width for the associated DropDown. This is useful when some items in the ActionGroup's DropDown are wider than usual and you don't want to make the ActionGroup widget itself wider. :attr:`dropdown_width` is an :class:`~kivy.properties.NumericProperty` and defaults to 0. .. versionadded:: 1.9.2 ''' def __init__(self, **kwargs): self.list_action_item = [] self._list_overflow_items = [] super(ActionGroup, self).__init__(**kwargs) self.dropdown_cls = ActionDropDown def add_widget(self, item): if isinstance(item, ActionSeparator): super(ActionGroup, self).add_widget(item) return if not isinstance(item, ActionItem): raise ActionBarException('ActionGroup only accepts ActionItem') self.list_action_item.append(item) def show_group(self): self.clear_widgets() for item in self._list_overflow_items + self.list_action_item: item.inside_group = True self._dropdown.add_widget(item) def _build_dropdown(self, *largs): if self._dropdown: self._dropdown.unbind(on_dismiss=self._toggle_dropdown) self._dropdown.dismiss() self._dropdown = None self._dropdown = self.dropdown_cls() self._dropdown.bind(on_dismiss=self._toggle_dropdown) def _update_dropdown(self, *largs): pass def _toggle_dropdown(self, *largs): self.is_open = not self.is_open ddn = self._dropdown ddn.size_hint_x = None if not ddn.container: return children = ddn.container.children if children: ddn.width = self.dropdown_width or max( self.width, max(c.pack_width for c in children)) else: ddn.width = self.width for item in children: item.size_hint_y = None item.height = max([self.height, sp(48)]) def clear_widgets(self): self._dropdown.clear_widgets()
class ActionView(BoxLayout): '''ActionView class, see module documentation for more information. ''' action_previous = ObjectProperty(None) '''Previous button for an ActionView. :attr:`action_previous` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' background_color = ListProperty([1, 1, 1, 1]) '''Background color in the format (r, g, b, a). :attr:`background_color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1]. ''' background_image = StringProperty( 'atlas://data/images/defaulttheme/action_view') '''Background image of an ActionViews default graphical representation. :attr:`background_image` is an :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/action_view'. ''' use_separator = BooleanProperty(False) '''Specify whether to use a separator before every ActionGroup or not. :attr:`use_separator` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' overflow_group = ObjectProperty(None) '''Widget to be used for the overflow. :attr:`overflow_group` is an :class:`~kivy.properties.ObjectProperty` and defaults to an instance of :class:`ActionOverflow`. ''' def __init__(self, **kwargs): self._list_action_items = [] self._list_action_group = [] super(ActionView, self).__init__(**kwargs) self._state = '' if not self.overflow_group: self.overflow_group = ActionOverflow( use_separator=self.use_separator) def on_action_previous(self, instance, value): self._list_action_items.insert(0, value) def add_widget(self, action_item, index=0): if action_item is None: return if not isinstance(action_item, ActionItem): raise ActionBarException('ActionView only accepts ActionItem' ' (got {!r}'.format(action_item)) elif isinstance(action_item, ActionOverflow): self.overflow_group = action_item action_item.use_separator = self.use_separator elif isinstance(action_item, ActionGroup): self._list_action_group.append(action_item) action_item.use_separator = self.use_separator elif isinstance(action_item, ActionPrevious): self.action_previous = action_item else: super(ActionView, self).add_widget(action_item, index) if index == 0: index = len(self._list_action_items) self._list_action_items.insert(index, action_item) def on_use_separator(self, instance, value): for group in self._list_action_group: group.use_separator = value self.overflow_group.use_separator = value def remove_widget(self, widget): super(ActionView, self).remove_widget(widget) if isinstance(widget, ActionOverflow): for item in widget.list_action_item: self._list_action_items.remove(item) if widget in self._list_action_items: self._list_action_items.remove(widget) def _clear_all(self): lst = self._list_action_items[:] self.clear_widgets() for group in self._list_action_group: group.clear_widgets() self.overflow_group.clear_widgets() self.overflow_group.list_action_item = [] self._list_action_items = lst def _layout_all(self): # all the items can fit to the view, so expand everything super_add = super(ActionView, self).add_widget self._state = 'all' self._clear_all() if not self.action_previous.parent: super_add(self.action_previous) if len(self._list_action_items) > 1: for child in self._list_action_items[1:]: child.inside_group = False super_add(child) for group in self._list_action_group: if group.mode == 'spinner': super_add(group) group.show_group() else: if group.list_action_item != []: super_add(ActionSeparator()) for child in group.list_action_item: child.inside_group = False super_add(child) self.overflow_group.show_default_items(self) def _layout_group(self): # layout all the items in order to pack them per group super_add = super(ActionView, self).add_widget self._state = 'group' self._clear_all() if not self.action_previous.parent: super_add(self.action_previous) if len(self._list_action_items) > 1: for child in self._list_action_items[1:]: super_add(child) child.inside_group = False for group in self._list_action_group: super_add(group) group.show_group() self.overflow_group.show_default_items(self) def _layout_random(self): # layout the items in order to pack all of them grouped, and display # only the action items having 'important' super_add = super(ActionView, self).add_widget self._state = 'random' self._clear_all() hidden_items = [] hidden_groups = [] total_width = 0 if not self.action_previous.parent: super_add(self.action_previous) width = (self.width - self.overflow_group.pack_width - self.action_previous.minimum_width) if len(self._list_action_items): for child in self._list_action_items[1:]: if child.important: if child.pack_width + total_width < width: super_add(child) child.inside_group = False total_width += child.pack_width else: hidden_items.append(child) else: hidden_items.append(child) # if space is left then display ActionItem inside their # ActionGroup if total_width < self.width: for group in self._list_action_group: if group.pack_width + total_width +\ group.separator_width < width: super_add(group) group.show_group() total_width += (group.pack_width + group.separator_width) else: hidden_groups.append(group) group_index = len(self.children) - 1 # if space is left then display other ActionItems if total_width < self.width: for child in hidden_items[:]: if child.pack_width + total_width < width: super_add(child, group_index) total_width += child.pack_width child.inside_group = False hidden_items.remove(child) # for all the remaining ActionItems and ActionItems with in # ActionGroups, Display them inside overflow_group extend_hidden = hidden_items.extend for group in hidden_groups: extend_hidden(group.list_action_item) overflow_group = self.overflow_group if hidden_items != []: over_add = super(overflow_group.__class__, overflow_group).add_widget for child in hidden_items: over_add(child) overflow_group.show_group() if not self.overflow_group.parent: super_add(overflow_group) def on_width(self, width, *args): # determine the layout to use # can we display all of them? total_width = 0 for child in self._list_action_items: total_width += child.pack_width for group in self._list_action_group: for child in group.list_action_item: total_width += child.pack_width if total_width <= self.width: if self._state != 'all': self._layout_all() return # can we display them per group? total_width = 0 for child in self._list_action_items: total_width += child.pack_width for group in self._list_action_group: total_width += group.pack_width if total_width < self.width: # ok, we can display all the items grouped if self._state != 'group': self._layout_group() return # none of the solutions worked, display them in pack mode self._layout_random()
class BaseButton( ThemableBehavior, ButtonBehavior, SpecificBackgroundColorBehavior, AnchorLayout, Widget, ): """ Abstract base class for all MD buttons. This class handles the button's colors (disabled/down colors handled in children classes as those depend on type of button) as well as the disabled state. """ theme_text_color = OptionProperty( "Primary", options=[ "Primary", "Secondary", "Hint", "Error", "Custom", "ContrastParentBackground", ], ) """ Button text type. Available options are: (`"Primary"`, `"Secondary"`, `"Hint"`, `"Error"`, `"Custom"`, `"ContrastParentBackground"`). :attr:`theme_text_color` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Primary'`. """ text_color = ListProperty() """ Text color in ``rgba`` format. :attr:`text_color` is an :class:`~kivy.properties.ListProperty` and defaults to `''`. """ font_name = StringProperty() """ Font name. :attr:`font_name` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ font_size = NumericProperty(14) """ Font size. :attr:`font_size` is an :class:`~kivy.properties.NumericProperty` and defaults to `14`. """ user_font_size = NumericProperty() """Custom font size for :class:`~MDIconButton`. :attr:`user_font_size` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ md_bg_color_disabled = ListProperty() """Color disabled. :attr:`md_bg_color_disabled` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ line_width = NumericProperty(1) opposite_colors = BooleanProperty(False) _current_button_color = ListProperty([0.0, 0.0, 0.0, 0.0]) _current_text_color = ListProperty([1.0, 1.0, 1.0, 1]) _md_bg_color_down = ListProperty([0.0, 0.0, 0.0, 0.1]) _md_bg_color_disabled = ListProperty([0.0, 0.0, 0.0, 0.0]) def __init__(self, **kwargs): super().__init__(**kwargs) self.theme_cls.bind(primary_palette=self.update_md_bg_color) Clock.schedule_once(self.check_current_button_color) def update_md_bg_color(self, instance, value): """Called when the application color palette changes.""" def check_current_button_color(self, interval): if self.md_bg_color_disabled: self._md_bg_color_disabled = self.md_bg_color_disabled else: self._md_bg_color_disabled = self.theme_cls.disabled_hint_text_color self.on_disabled(self, self.disabled) if self._current_button_color == [0.0, 0.0, 0.0, 0.0]: self._current_button_color = self.md_bg_color def on_text_color(self, instance, value): if value not in ([0, 0, 0, 0.87], [1.0, 1.0, 1.0, 1]): self._current_text_color = value def on_md_bg_color(self, instance, value): if value != self.theme_cls.primary_color: self._current_button_color = value def on_disabled(self, instance, value): if self.disabled: self._current_button_color = self._md_bg_color_disabled else: self._current_button_color = self.md_bg_color def on_font_size(self, instance, value): def _on_font_size(interval): if "lbl_ic" in instance.ids: instance.ids.lbl_ic.font_size = sp(value) Clock.schedule_once(_on_font_size)
class ActionPrevious(BoxLayout, ActionItem): '''ActionPrevious class, see module documentation for more information. ''' with_previous = BooleanProperty(True) '''Specifies whether clicking on ActionPrevious will load the previous screen or not. If True, the previous_icon will be shown otherwise it will not. :attr:`with_previous` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' app_icon = StringProperty(window_icon) '''Application icon for the ActionView. :attr:`app_icon` is a :class:`~kivy.properties.StringProperty` and defaults to the window icon if set, otherwise 'data/logo/kivy-icon-32.png'. ''' app_icon_width = NumericProperty(0) '''Width of app_icon image. ''' app_icon_height = NumericProperty(0) '''Height of app_icon image. ''' color = ListProperty([1, 1, 1, 1]) '''Text color, in the format (r, g, b, a) :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1]. ''' previous_image = StringProperty( 'atlas://data/images/defaulttheme/previous_normal') '''Image for the 'previous' ActionButtons default graphical representation. :attr:`previous_image` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/previous_normal'. ''' previous_image_width = NumericProperty(0) '''Width of previous_image image. ''' previous_image_height = NumericProperty(0) '''Height of previous_image image. ''' title = StringProperty('') '''Title for ActionView. :attr:`title` is a :class:`~kivy.properties.StringProperty` and defaults to ''. ''' def __init__(self, **kwargs): self.register_event_type('on_press') self.register_event_type('on_release') super(ActionPrevious, self).__init__(**kwargs) if not self.app_icon: self.app_icon = 'data/logo/kivy-icon-32.png' def on_press(self): pass def on_release(self): pass
class MDFloatingActionButtonSpeedDial(ThemableBehavior, FloatLayout): """ :Events: :attr:`on_open` Called when a stack is opened. :attr:`on_close` Called when a stack is closed. """ icon = StringProperty("plus") """ Root button icon name. :attr:`icon` is a :class:`~kivy.properties.StringProperty` and defaults to `'plus'`. """ anchor = OptionProperty("right", option=["right"]) """ Stack anchor. Available options are: `'right'`. :attr:`anchor` is a :class:`~kivy.properties.OptionProperty` and defaults to `'right'`. """ callback = ObjectProperty(lambda x: None) """ Custom callback. .. code-block:: kv MDFloatingActionButtonSpeedDial: callback: app.callback .. code-block:: python def callback(self, instance): print(instance.icon) :attr:`callback` is a :class:`~kivy.properties.ObjectProperty` and defaults to `None`. """ label_text_color = ListProperty([0, 0, 0, 1]) """ Floating text color in ``rgba`` format. :attr:`label_text_color` is a :class:`~kivy.properties.ListProperty` and defaults to `[0, 0, 0, 1]`. """ data = DictProperty() """ Must be a dictionary .. code-block:: python { 'name-icon': 'Text label', ..., ..., } """ right_pad = BooleanProperty(True) """ If `True`, the button will increase on the right side by 2.5 piesels if the :attr:`~hint_animation` parameter equal to `True`. .. rubric:: False .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-right-pad.gif :align: center .. rubric:: True .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-right-pad-true.gif :align: center :attr:`right_pad` is a :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ rotation_root_button = BooleanProperty(False) """ If ``True`` then the root button will rotate 45 degrees when the stack is opened. :attr:`rotation_root_button` is a :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ opening_transition = StringProperty("out_cubic") """ The name of the stack opening animation type. :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty` and defaults to `'out_cubic'`. """ closing_transition = StringProperty("out_cubic") """ The name of the stack closing animation type. :attr:`closing_transition` is a :class:`~kivy.properties.StringProperty` and defaults to `'out_cubic'`. """ opening_transition_button_rotation = StringProperty("out_cubic") """ The name of the animation type to rotate the root button when opening the stack. :attr:`opening_transition_button_rotation` is a :class:`~kivy.properties.StringProperty` and defaults to `'out_cubic'`. """ closing_transition_button_rotation = StringProperty("out_cubic") """ The name of the animation type to rotate the root button when closing the stack. :attr:`closing_transition_button_rotation` is a :class:`~kivy.properties.StringProperty` and defaults to `'out_cubic'`. """ opening_time = NumericProperty(0.5) """ Time required for the stack to go to: attr:`state` `'open'`. :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty` and defaults to `0.2`. """ closing_time = NumericProperty(0.2) """ Time required for the stack to go to: attr:`state` `'close'`. :attr:`closing_time` is a :class:`~kivy.properties.NumericProperty` and defaults to `0.2`. """ opening_time_button_rotation = NumericProperty(0.2) """ Time required to rotate the root button 45 degrees during the stack opening animation. :attr:`opening_time_button_rotation` is a :class:`~kivy.properties.NumericProperty` and defaults to `0.2`. """ closing_time_button_rotation = NumericProperty(0.2) """ Time required to rotate the root button 0 degrees during the stack closing animation. :attr:`closing_time_button_rotation` is a :class:`~kivy.properties.NumericProperty` and defaults to `0.2`. """ state = OptionProperty("close", options=("close", "open")) """ Indicates whether the stack is closed or open. Available options are: `'close'`, `'open'`. :attr:`state` is a :class:`~kivy.properties.OptionProperty` and defaults to `'close'`. """ bg_color_root_button = ListProperty() """ Root button color in ``rgba`` format. :attr:`bg_color_root_button` is a :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ bg_color_stack_button = ListProperty() """ The color of the buttons in the stack ``rgba`` format. :attr:`bg_color_stack_button` is a :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ color_icon_stack_button = ListProperty() """ The color icon of the buttons in the stack ``rgba`` format. :attr:`color_icon_stack_button` is a :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ color_icon_root_button = ListProperty() """ The color icon of the root button ``rgba`` format. :attr:`color_icon_root_button` is a :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ bg_hint_color = ListProperty() """ Background color for the text of the buttons in the stack ``rgba`` format. :attr:`bg_hint_color` is a :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ hint_animation = BooleanProperty(False) """ Whether to use button extension animation to display text labels. :attr:`hint_animation` is a :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ _label_pos_y_set = False _anim_buttons_data = {} _anim_labels_data = {} def __init__(self, **kwargs): super().__init__(**kwargs) self.register_event_type("on_open") self.register_event_type("on_close") Window.bind(on_resize=self._update_pos_buttons) def on_open(self, *args): """Called when a stack is opened.""" def on_close(self, *args): """Called when a stack is closed.""" def on_leave(self, instance): """Called when the mouse cursor goes outside the button of stack.""" if self.state == "open": for widget in self.children: if isinstance(widget, MDFloatingLabel) and self.hint_animation: Animation.cancel_all(widget) if self.data[instance.icon] == widget.text: Animation( _canvas_width=0, _padding_right=0, d=self.opening_time, t=self.opening_transition, ).start(instance) if self.hint_animation: Animation( opacity=0, d=0.1, t=self.opening_transition ).start(widget) break def on_enter(self, instance): """Called when the mouse cursor is over a button from the stack.""" if self.state == "open": for widget in self.children: if isinstance(widget, MDFloatingLabel) and self.hint_animation: widget.elevation = 0 if self.data[instance.icon] == widget.text: Animation( _canvas_width=widget.width + dp(24), _padding_right=dp(5) if self.right_pad else 0, d=self.opening_time, t=self.opening_transition, ).start(instance) if self.hint_animation: Animation( opacity=1, d=self.opening_time, t=self.opening_transition, ).start(widget) break def on_data(self, instance, value): """Creates a stack of buttons.""" # Bottom buttons. for name_icon in self.data.keys(): bottom_button = MDFloatingBottomButton( icon=name_icon, on_enter=self.on_enter, on_leave=self.on_leave, opacity=0, ) bottom_button.bind( on_release=lambda x=bottom_button: self.callback(x) ) self.set_pos_bottom_buttons(bottom_button) self.add_widget(bottom_button) # Labels. floating_text = value[name_icon] if floating_text: label = MDFloatingLabel(text=floating_text, opacity=0) label.text_color = self.label_text_color self.add_widget(label) # Top root button. root_button = MDFloatingRootButton(on_release=self.open_stack) root_button.icon = self.icon self.set_pos_root_button(root_button) self.add_widget(root_button) def on_icon(self, instance, value): self._get_count_widget(MDFloatingRootButton).icon = value def on_label_text_color(self, instance, value): for widget in self.children: if isinstance(widget, MDFloatingLabel): widget.text_color = value def on_color_icon_stack_button(self, instance, value): for widget in self.children: if isinstance(widget, MDFloatingBottomButton): widget.text_color = value def on_hint_animation(self, instance, value): for widget in self.children: if isinstance(widget, MDFloatingLabel): widget.bg_color = (0, 0, 0, 0) def on_bg_hint_color(self, instance, value): for widget in self.children: if isinstance(widget, MDFloatingBottomButton): widget._bg_color = value def on_color_icon_root_button(self, instance, value): self._get_count_widget(MDFloatingRootButton).text_color = value def on_bg_color_stack_button(self, instance, value): for widget in self.children: if isinstance(widget, MDFloatingBottomButton): widget.md_bg_color = value def on_bg_color_root_button(self, instance, value): self._get_count_widget(MDFloatingRootButton).md_bg_color = value def set_pos_labels(self, widget): """Sets the position of the floating labels.""" if self.anchor == "right": widget.x = Window.width - widget.width - dp(86) def set_pos_root_button(self, instance): """Sets the position of the root button.""" if self.anchor == "right": instance.y = dp(20) instance.x = Window.width - (dp(56) + dp(20)) def set_pos_bottom_buttons(self, instance): """Sets the position of the bottom buttons in a stack.""" if self.anchor == "right": if self.state != "open": instance.y = instance.height / 2 instance.x = Window.width - (instance.height + instance.width / 2) def open_stack(self, instance): """Opens a button stack.""" for widget in self.children: if isinstance(widget, MDFloatingLabel): Animation.cancel_all(widget) if self.state != "open": y = 0 label_position = dp(56) anim_buttons_data = {} anim_labels_data = {} for widget in self.children: if isinstance(widget, MDFloatingBottomButton): # Sets new button positions. y += dp(56) widget.y = widget.y * 2 + y if not self._anim_buttons_data: anim_buttons_data[widget] = Animation( opacity=1, d=self.opening_time, t=self.opening_transition, ) elif isinstance(widget, MDFloatingLabel): # Sets new labels positions. label_position += dp(56) # Sets the position of signatures only once. if not self._label_pos_y_set: widget.y = widget.y * 2 + label_position widget.x = Window.width - widget.width - dp(86) if not self._anim_labels_data: anim_labels_data[widget] = Animation( opacity=1, d=self.opening_time ) elif ( isinstance(widget, MDFloatingRootButton) and self.rotation_root_button ): # Rotates the root button 45 degrees. Animation( _angle=-45, d=self.opening_time_button_rotation, t=self.opening_transition_button_rotation, ).start(widget) if anim_buttons_data: self._anim_buttons_data = anim_buttons_data if anim_labels_data and not self.hint_animation: self._anim_labels_data = anim_labels_data self.state = "open" self.dispatch("on_open") self.do_animation_open_stack(self._anim_buttons_data) self.do_animation_open_stack(self._anim_labels_data) if not self._label_pos_y_set: self._label_pos_y_set = True else: self.close_stack() def do_animation_open_stack(self, anim_data): def on_progress(animation, widget, value): if value >= 0.1: animation_open_stack() def animation_open_stack(*args): try: widget = next(widgets_list) animation = anim_data[widget] animation.bind(on_progress=on_progress) animation.start(widget) except StopIteration: pass widgets_list = iter(list(anim_data.keys())) animation_open_stack() def close_stack(self): """Closes the button stack.""" for widget in self.children: if isinstance(widget, MDFloatingBottomButton): Animation( y=widget.height / 2, d=self.closing_time, t=self.closing_transition, opacity=0, ).start(widget) elif isinstance(widget, MDFloatingLabel): Animation(opacity=0, d=0.1).start(widget) elif ( isinstance(widget, MDFloatingRootButton) and self.rotation_root_button ): Animation( _angle=0, d=self.closing_time_button_rotation, t=self.closing_transition_button_rotation, ).start(widget) self.state = "close" self.dispatch("on_close") def _update_pos_buttons(self, instance, width, height): # Updates button positions when resizing screen. for widget in self.children: if isinstance(widget, MDFloatingBottomButton): self.set_pos_bottom_buttons(widget) elif isinstance(widget, MDFloatingRootButton): self.set_pos_root_button(widget) elif isinstance(widget, MDFloatingLabel): self.set_pos_labels(widget) def _get_count_widget(self, instance): widget = None for widget in self.children: if isinstance(widget, instance): break return widget
class MDTextFieldRound(ThemableBehavior, BoxLayout): __events__ = ("on_text_validate", "on_text", "on_focus") write_tab = BooleanProperty(False) """write_tab property of TextInput""" input_filter = ObjectProperty(None) """input_filter from TextInput""" readonly = BooleanProperty(False) """readonly property from TextInput""" tab_width = NumericProperty(4) """tab_width property from TextInput""" text_language = StringProperty() """text_language property from TextInput""" """font related properties from TextInput""" font_context = StringProperty() font_family = StringProperty() font_name = StringProperty("Roboto") font_size = NumericProperty(15) allow_copy = BooleanProperty(True) """Whether copying text from the field is allowed or not""" width = NumericProperty(Window.width - dp(100)) """Text field width.""" icon_left = StringProperty("email-outline") """Left icon.""" icon_right = StringProperty("email-outline") """Right icon.""" icon_type = OptionProperty("none", options=["right", "left", "all", "without"]) """Use one (left) or two (left and right) icons in the text field.""" hint_text = StringProperty() """Hint text in the text field.""" icon_color = ListProperty([1, 1, 1, 1]) """Color of icons.""" active_color = ListProperty([1, 1, 1, 0.2]) """The color of the text field when it is in focus.""" normal_color = ListProperty([1, 1, 1, 0.5]) """The color of the text field when it not in focus.""" foreground_color = ListProperty([1, 1, 1, 1]) """Text color.""" hint_text_color = ListProperty([0.5, 0.5, 0.5, 1.0]) """Text field hint color.""" cursor_color = ListProperty() """Color of cursor""" selection_color = ListProperty() """Text selection color.""" icon_callback = ObjectProperty() """The function that is called when you click on the icon in the text field.""" text = StringProperty() """Text of field.""" icon_left_disabled = BooleanProperty(True) """Disable the left icon.""" icon_right_disabled = BooleanProperty(False) """Disable the right icon.""" password = BooleanProperty(False) """Hide text or notю""" password_mask = StringProperty("*") """Characters on which the text will be replaced if the `password` is True.""" require_text_error = StringProperty() """Error text if the text field requires mandatory text.""" require_error_callback = ObjectProperty() """The function that will be called when the unfocus. if `require_text_error` != '' """ event_focus = ObjectProperty() """The function is called at the moment of focus/unfocus of the text field. """ focus = BooleanProperty() """Whether or not the widget is focused""" error_color = ListProperty( [0.7607843137254902, 0.2235294117647059, 0.2549019607843137, 1]) _current_color = ListProperty() _outline_color = ListProperty([0, 0, 0, 0]) _instance_icon_left = ObjectProperty() _instance_icon_right = ObjectProperty() def __init__(self, **kwargs): super().__init__(**kwargs) if not (len(self.cursor_color)): self.cursor_color = self.theme_cls.primary_color if not (len(self.selection_color)): self.selection_color = self.theme_cls.primary_color self.selection_color[3] = 0.75 self._current_color = self.normal_color def on_icon_type(self, instance, value): def remove_icon_right(): self.ids.box.remove_widget(self.ids.icon_right) self.add_widget(Widget(size_hint_x=None, width=dp(48))) if value == "left": remove_icon_right() elif value == "right": self.ids.box.remove_widget(self.ids.icon_left) elif value == "without": remove_icon_right() self.ids.box.remove_widget(self.ids.icon_left) self.add_widget(Widget(size_hint_x=None, width=dp(48)), index=0) def on_normal_color(self, instance, value): self._current_color = value def get_color_line(self, field_inastance, field_text, field_focus): if not field_focus: if self.require_text_error and field_text == "": self._outline_color = self.error_color self._instance_icon_left.text_color = self._outline_color if self.require_text_error != "": self.show_require_error() else: self._outline_color = [0, 0, 0, 0] else: self._outline_color = self.theme_cls.primary_color def show_require_error(self): self.ids.label_error_require.text = self.require_text_error self.ids.spacer.height = "10dp" if self.require_error_callback: self.require_error_callback(self) def hide_require_error(self, focus): if focus: self.ids.label_error_require.text = "" self.ids.spacer.height = 0 """ TextInput Events""" def on_text_validate(self): pass def on_text(self, *args): pass def on_focus(self, *args): pass
class MDTextField(ThemableBehavior, FixedHintTextInput): helper_text = StringProperty("This field is required") helper_text_mode = OptionProperty( "none", options=["none", "on_error", "persistent", "on_focus"]) max_text_length = NumericProperty(None) required = BooleanProperty(False) color_mode = OptionProperty("primary", options=["primary", "accent", "custom"]) line_color_normal = ListProperty() line_color_focus = ListProperty() error_color = ListProperty() error = BooleanProperty(False) _text_len_error = BooleanProperty(False) _hint_lbl_font_size = NumericProperty(sp(16)) _hint_y = NumericProperty(dp(38)) _line_width = NumericProperty(0) _current_line_color = ListProperty([0.0, 0.0, 0.0, 0.0]) _current_error_color = ListProperty([0.0, 0.0, 0.0, 0.0]) _current_hint_text_color = ListProperty([0.0, 0.0, 0.0, 0.0]) _current_right_lbl_color = ListProperty([0.0, 0.0, 0.0, 0.0]) def __init__(self, **kwargs): self._msg_lbl = TextfieldLabel( font_style="Caption", halign="left", valign="middle", text=self.helper_text, ) self._right_msg_lbl = TextfieldLabel(font_style="Caption", halign="right", valign="middle", text="") self._hint_lbl = TextfieldLabel(font_style="Subtitle1", halign="left", valign="middle") super().__init__(**kwargs) self.line_color_normal = self.theme_cls.divider_color self.line_color_focus = self.theme_cls.primary_color self.error_color = self.theme_cls.error_color self._current_hint_text_color = self.theme_cls.disabled_hint_text_color self._current_line_color = self.theme_cls.primary_color self.bind( helper_text=self._set_msg, hint_text=self._set_hint, _hint_lbl_font_size=self._hint_lbl.setter("font_size"), helper_text_mode=self._set_message_mode, max_text_length=self._set_max_text_length, text=self.on_text, ) self.theme_cls.bind( primary_color=self._update_primary_color, theme_style=self._update_theme_style, accent_color=self._update_accent_color, ) self.has_had_text = False def _update_colors(self, color): self.line_color_focus = color if not self.error and not self._text_len_error: self._current_line_color = color if self.focus: self._current_line_color = color def _update_accent_color(self, *args): if self.color_mode == "accent": self._update_colors(self.theme_cls.accent_color) def _update_primary_color(self, *args): if self.color_mode == "primary": self._update_colors(self.theme_cls.primary_color) def _update_theme_style(self, *args): self.line_color_normal = self.theme_cls.divider_color if not any([self.error, self._text_len_error]): if not self.focus: self._current_hint_text_color = ( self.theme_cls.disabled_hint_text_color) self._current_right_lbl_color = ( self.theme_cls.disabled_hint_text_color) if self.helper_text_mode == "persistent": self._current_error_color = ( self.theme_cls.disabled_hint_text_color) def on_width(self, instance, width): if (any([self.focus, self.error, self._text_len_error]) and instance is not None): self._line_width = width self._msg_lbl.width = self.width self._right_msg_lbl.width = self.width self._hint_lbl.width = self.width def on_focus(self, *args): disabled_hint_text_color = self.theme_cls.disabled_hint_text_color Animation.cancel_all(self, "_line_width", "_hint_y", "_hint_lbl_font_size") if self.max_text_length is None: max_text_length = sys.maxsize else: max_text_length = self.max_text_length if len(self.text) > max_text_length or all( [self.required, len(self.text) == 0, self.has_had_text]): self._text_len_error = True if self.error or all([ self.max_text_length is not None and len(self.text) > self.max_text_length ]): has_error = True else: if all([self.required, len(self.text) == 0, self.has_had_text]): has_error = True else: has_error = False if self.focus: self.has_had_text = True Animation.cancel_all(self, "_line_width", "_hint_y", "_hint_lbl_font_size") if not self.text: Animation( _hint_y=dp(14), _hint_lbl_font_size=sp(12), duration=0.2, t="out_quad", ).start(self) Animation(_line_width=self.width, duration=0.2, t="out_quad").start(self) if has_error: Animation( duration=0.2, _current_hint_text_color=self.error_color, _current_right_lbl_color=self.error_color, _current_line_color=self.error_color, ).start(self) if self.helper_text_mode == "on_error" and ( self.error or self._text_len_error): Animation( duration=0.2, _current_error_color=self.error_color).start(self) elif (self.helper_text_mode == "on_error" and not self.error and not self._text_len_error): Animation(duration=0.2, _current_error_color=(0, 0, 0, 0)).start(self) elif self.helper_text_mode == "persistent": Animation( duration=0.2, _current_error_color=disabled_hint_text_color, ).start(self) elif self.helper_text_mode == "on_focus": Animation( duration=0.2, _current_error_color=disabled_hint_text_color, ).start(self) else: Animation( duration=0.2, _current_hint_text_color=self.line_color_focus, _current_right_lbl_color=disabled_hint_text_color, ).start(self) if self.helper_text_mode == "on_error": Animation(duration=0.2, _current_error_color=(0, 0, 0, 0)).start(self) if self.helper_text_mode == "persistent": Animation( duration=0.2, _current_error_color=disabled_hint_text_color, ).start(self) elif self.helper_text_mode == "on_focus": Animation( duration=0.2, _current_error_color=disabled_hint_text_color, ).start(self) else: if not self.text: Animation( _hint_y=dp(38), _hint_lbl_font_size=sp(16), duration=0.2, t="out_quad", ).start(self) if has_error: Animation( duration=0.2, _current_line_color=self.error_color, _current_hint_text_color=self.error_color, _current_right_lbl_color=self.error_color, ).start(self) if self.helper_text_mode == "on_error" and ( self.error or self._text_len_error): Animation( duration=0.2, _current_error_color=self.error_color).start(self) elif (self.helper_text_mode == "on_error" and not self.error and not self._text_len_error): Animation(duration=0.2, _current_error_color=(0, 0, 0, 0)).start(self) elif self.helper_text_mode == "persistent": Animation( duration=0.2, _current_error_color=disabled_hint_text_color, ).start(self) elif self.helper_text_mode == "on_focus": Animation(duration=0.2, _current_error_color=(0, 0, 0, 0)).start(self) else: Animation( duration=0.2, _current_line_color=self.line_color_focus, _current_hint_text_color=disabled_hint_text_color, _current_right_lbl_color=(0, 0, 0, 0), ).start(self) if self.helper_text_mode == "on_error": Animation(duration=0.2, _current_error_color=(0, 0, 0, 0)).start(self) elif self.helper_text_mode == "persistent": Animation( duration=0.2, _current_error_color=disabled_hint_text_color, ).start(self) elif self.helper_text_mode == "on_focus": Animation(duration=0.2, _current_error_color=(0, 0, 0, 0)).start(self) Animation(_line_width=0, duration=0.2, t="out_quad").start(self) def on_text(self, instance, text): if len(text) > 0: self.has_had_text = True if self.max_text_length is not None: self._right_msg_lbl.text = f"{len(text)}/{self.max_text_length}" max_text_length = self.max_text_length else: max_text_length = sys.maxsize if len(text) > max_text_length or all( [self.required, len(self.text) == 0, self.has_had_text]): self._text_len_error = True else: self._text_len_error = False if self.error or self._text_len_error: if self.focus: Animation( duration=0.2, _current_hint_text_color=self.error_color, _current_line_color=self.error_color, ).start(self) if self.helper_text_mode == "on_error" and ( self.error or self._text_len_error): Animation( duration=0.2, _current_error_color=self.error_color).start(self) if self._text_len_error: Animation( duration=0.2, _current_right_lbl_color=self.error_color).start(self) else: if self.focus: disabled_hint_text_color = ( self.theme_cls.disabled_hint_text_color) Animation( duration=0.2, _current_right_lbl_color=disabled_hint_text_color, ).start(self) Animation( duration=0.2, _current_hint_text_color=self.line_color_focus, _current_line_color=self.line_color_focus, ).start(self) if self.helper_text_mode == "on_error": Animation(duration=0.2, _current_error_color=(0, 0, 0, 0)).start(self) if len(self.text) != 0 and not self.focus: self._hint_y = dp(14) self._hint_lbl_font_size = sp(12) def on_text_validate(self): self.has_had_text = True if self.max_text_length is None: max_text_length = sys.maxsize else: max_text_length = self.max_text_length if len(self.text) > max_text_length or all( [self.required, len(self.text) == 0, self.has_had_text]): self._text_len_error = True def _set_hint(self, instance, text): self._hint_lbl.text = text def _set_msg(self, instance, text): self._msg_lbl.text = text self.helper_text = text def _set_message_mode(self, instance, text): self.helper_text_mode = text if self.helper_text_mode == "persistent": disabled_hint_text_color = self.theme_cls.disabled_hint_text_color Animation( duration=0.1, _current_error_color=disabled_hint_text_color).start(self) def _set_max_text_length(self, instance, length): self.max_text_length = length self._right_msg_lbl.text = f"{len(self.text)}/{length}" def on_color_mode(self, instance, mode): if mode == "primary": self._update_primary_color() elif mode == "accent": self._update_accent_color() elif mode == "custom": self._update_colors(self.line_color_focus) def on_line_color_focus(self, *args): if self.color_mode == "custom": self._update_colors(self.line_color_focus)
class MDLabel(ThemableBehavior, Label): font_style = OptionProperty("Body1", options=theme_font_styles) """ Label font style. Available options are: `'H1'`, `'H2'`, `'H3'`, `'H4'`, `'H5'`, `'H6'`, `'Subtitle1'`, `'Subtitle2'`, `'Body1'`, `'Body2'`, `'Button'`, `'Caption'`, `'Overline'`, `'Icon'`. :attr:`font_style` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Body1'`. """ _capitalizing = BooleanProperty(False) def _get_text(self): if self._capitalizing: return self._text.upper() return self._text def _set_text(self, value): self._text = value _text = StringProperty() text = AliasProperty(_get_text, _set_text, bind=["_text", "_capitalizing"]) """Text of the label.""" theme_text_color = OptionProperty( "Primary", allownone=True, options=[ "Primary", "Secondary", "Hint", "Error", "Custom", "ContrastParentBackground", ], ) """ Label color scheme name. Available options are: `'Primary'`, `'Secondary'`, `'Hint'`, `'Error'`, `'Custom'`, `'ContrastParentBackground'`. :attr:`theme_text_color` is an :class:`~kivy.properties.OptionProperty` and defaults to `None`. """ text_color = ListProperty(None, allownone=True) """Label text color in ``rgba`` format. :attr:`text_color` is an :class:`~kivy.properties.ListProperty` and defaults to `None`. """ parent_background = ListProperty(None, allownone=True) _currently_bound_property = {} can_capitalize = BooleanProperty(True) def __init__(self, **kwargs): super().__init__(**kwargs) self.bind( font_style=self.update_font_style, can_capitalize=self.update_font_style, ) self.on_theme_text_color(None, self.theme_text_color) self.update_font_style() self.on_opposite_colors(None, self.opposite_colors) def update_font_style(self, *args): font_info = self.theme_cls.font_styles[self.font_style] self.font_name = font_info[0] self.font_size = sp(font_info[1]) if font_info[2] and self.can_capitalize: self._capitalizing = True else: self._capitalizing = False # TODO: Add letter spacing change # self.letter_spacing = font_info[3] def on_theme_text_color(self, instance, value): t = self.theme_cls op = self.opposite_colors setter = self.setter("color") t.unbind(**self._currently_bound_property) attr_name = { "Primary": "text_color" if not op else "opposite_text_color", "Secondary": "secondary_text_color" if not op else "opposite_secondary_text_color", "Hint": "disabled_hint_text_color" if not op else "opposite_disabled_hint_text_color", "Error": "error_color", }.get(value, None) if attr_name: c = {attr_name: setter} t.bind(**c) self._currently_bound_property = c self.color = getattr(t, attr_name) else: # 'Custom' and 'ContrastParentBackground' lead here, as well as the # generic None value it's not yet been set if value == "Custom" and self.text_color: self.color = self.text_color elif value == "ContrastParentBackground" and self.parent_background: self.color = get_contrast_text_color(self.parent_background) else: self.color = [0, 0, 0, 1] def on_text_color(self, *args): if self.theme_text_color == "Custom": self.color = self.text_color def on_opposite_colors(self, instance, value): self.on_theme_text_color(self, self.theme_text_color)
class WindowBase(EventDispatcher): '''WindowBase is an abstract window widget for any window implementation. :Parameters: `borderless`: str, one of ('0', '1') Set the window border state. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. `fullscreen`: str, one of ('0', '1', 'auto', 'fake') Make the window fullscreen. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. `width`: int Width of the window. `height`: int Height of the window. :Events: `on_motion`: etype, motionevent Fired when a new :class:`~kivy.input.motionevent.MotionEvent` is dispatched `on_touch_down`: Fired when a new touch event is initiated. `on_touch_move`: Fired when an existing touch event changes location. `on_touch_up`: Fired when an existing touch event is terminated. `on_draw`: Fired when the :class:`Window` is being drawn. `on_flip`: Fired when the :class:`Window` GL surface is being flipped. `on_rotate`: rotation Fired when the :class:`Window` is being rotated. `on_close`: Fired when the :class:`Window` is closed. `on_request_close`: Fired when the event loop wants to close the window, or if the escape key is pressed and `exit_on_escape` is `True`. If a function bound to this event returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. .. versionadded:: 1.9.0 `on_keyboard`: key, scancode, codepoint, modifier Fired when the keyboard is used for input. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_down`: key, scancode, codepoint Fired when a key pressed. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_up`: key, scancode, codepoint Fired when a key is released. .. versionchanged:: 1.3.0 The *unicode* parameter has be deprecated in favor of codepoint, and will be removed completely in future versions. `on_dropfile`: str Fired when a file is dropped on the application. ''' __instance = None __initialized = False _fake_fullscreen = False # private properties _size = ListProperty([0, 0]) _modifiers = ListProperty([]) _rotation = NumericProperty(0) _clearcolor = ObjectProperty([0, 0, 0, 1]) children = ListProperty([]) '''List of the children of this window. :attr:`children` is a :class:`~kivy.properties.ListProperty` instance and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` to manipulate the list of children. Don't manipulate the list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this window. :attr:`parent` is a :class:`~kivy.properties.ObjectProperty` instance and defaults to None. When created, the parent is set to the window itself. You must take care of it if you are doing a recursive check. ''' icon = StringProperty() def _get_modifiers(self): return self._modifiers modifiers = AliasProperty(_get_modifiers, None) '''List of keyboard modifiers currently active. ''' def _get_size(self): r = self._rotation w, h = self._size if self.softinput_mode == 'resize': h -= self.keyboard_height if r in (0, 180): return w, h return h, w def _set_size(self, size): if self._size != size: r = self._rotation if r in (0, 180): self._size = size else: self._size = size[1], size[0] self.dispatch('on_resize', *size) return True else: return False size = AliasProperty(_get_size, _set_size, bind=('_size', )) '''Get the rotated size of the window. If :attr:`rotation` is set, then the size will change to reflect the rotation. ''' def _get_clearcolor(self): return self._clearcolor def _set_clearcolor(self, value): if value is not None: if type(value) not in (list, tuple): raise Exception('Clearcolor must be a list or tuple') if len(value) != 4: raise Exception('Clearcolor must contain 4 values') self._clearcolor = value clearcolor = AliasProperty(_get_clearcolor, _set_clearcolor, bind=('_clearcolor', )) '''Color used to clear the window. :: from kivy.core.window import Window # red background color Window.clearcolor = (1, 0, 0, 1) # don't clear background at all Window.clearcolor = None .. versionchanged:: 1.7.2 The clearcolor default value is now: (0, 0, 0, 1). ''' # make some property read-only def _get_width(self): r = self._rotation if r == 0 or r == 180: return self._size[0] return self._size[1] width = AliasProperty(_get_width, None, bind=('_rotation', '_size')) '''Rotated window width. :attr:`width` is a read-only :class:`~kivy.properties.AliasProperty`. ''' def _get_height(self): '''Rotated window height''' r = self._rotation kb = self.keyboard_height if self.softinput_mode == 'resize' else 0 if r == 0 or r == 180: return self._size[1] - kb return self._size[0] - kb height = AliasProperty(_get_height, None, bind=('_rotation', '_size')) '''Rotated window height. :attr:`height` is a read-only :class:`~kivy.properties.AliasProperty`. ''' def _get_center(self): return self.width / 2., self.height / 2. center = AliasProperty(_get_center, None, bind=('width', 'height')) '''Center of the rotated window. :attr:`center` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_rotation(self): return self._rotation def _set_rotation(self, x): x = int(x % 360) if x == self._rotation: return if x not in (0, 90, 180, 270): raise ValueError('can rotate only 0, 90, 180, 270 degrees') self._rotation = x if self.initialized is False: return self.dispatch('on_resize', *self.size) self.dispatch('on_rotate', x) rotation = AliasProperty(_get_rotation, _set_rotation, bind=('_rotation', )) '''Get/set the window content rotation. Can be one of 0, 90, 180, 270 degrees. ''' softinput_mode = OptionProperty('', options=('', 'pan', 'scale', 'resize')) '''This specifies the behavior of window contents on display of soft keyboard on mobile platform. Can be one of '', 'pan', 'scale', 'resize'. When '' The main window is left as it is allowing the user to use :attr:`keyboard_height` to manage the window contents the way they want. when 'pan' The main window pans moving the bottom part of the window to be always on top of the keyboard. when 'resize' The window is resized and the contents scaled to fit the remaining space. ..versionadded::1.9.0 :attr:`softinput_mode` is a :class:`OptionProperty` defaults to None. ''' _keyboard_changed = BooleanProperty(False) def _upd_kbd_height(self, *kargs): self._keyboard_changed = not self._keyboard_changed def _get_ios_kheight(self): return 0 def _get_android_kheight(self): global android if not android: import android return android.get_keyboard_height() def _get_kheight(self): if platform == 'android': return self._get_android_kheight() if platform == 'ios': return self._get_ios_kheight() return 0 keyboard_height = AliasProperty(_get_kheight, None, bind=('_keyboard_changed', )) '''Rerturns the height of the softkeyboard/IME on mobile platforms. Will return 0 if not on mobile platform or if IME is not active. ..versionadded:: 1.9.0 :attr:`keyboard_height` is a read-only :class:`AliasProperty` defaults to 0. ''' def _set_system_size(self, size): self._size = size def _get_system_size(self): if self.softinput_mode == 'resize': return self._size[0], self._size[1] - self.keyboard_height return self._size system_size = AliasProperty(_get_system_size, _set_system_size, bind=('_size', )) '''Real size of the window ignoring rotation. ''' borderless = BooleanProperty(False) '''When set to True, this property removes the window border/decoration. .. versionadded:: 1.9.0 :attr:`borderless` is a :class:`BooleanProperty`, defaults to False. ''' fullscreen = OptionProperty(False, options=(True, False, 'auto', 'fake')) '''This property sets the fullscreen mode of the window. Available options are: True, False, 'auto', 'fake'. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. .. versionadded:: 1.2.0 .. note:: The 'fake' option has been deprecated, use the :attr:`borderless` property instead. ''' mouse_pos = ObjectProperty([0, 0]) '''2d position of the mouse within the window. .. versionadded:: 1.2.0 ''' @property def __self__(self): return self top = NumericProperty(None, allownone=True) left = NumericProperty(None, allownone=True) position = OptionProperty('auto', options=['auto', 'custom']) render_context = ObjectProperty(None) canvas = ObjectProperty(None) title = StringProperty('Kivy') __events__ = ('on_draw', 'on_flip', 'on_rotate', 'on_resize', 'on_close', 'on_motion', 'on_touch_down', 'on_touch_move', 'on_touch_up', 'on_mouse_down', 'on_mouse_move', 'on_mouse_up', 'on_keyboard', 'on_key_down', 'on_key_up', 'on_textinput', 'on_dropfile', 'on_request_close', 'on_joy_axis', 'on_joy_hat', 'on_joy_ball', 'on_joy_button_down', "on_joy_button_up") def __new__(cls, **kwargs): if cls.__instance is None: cls.__instance = EventDispatcher.__new__(cls) return cls.__instance def __init__(self, **kwargs): force = kwargs.pop('force', False) # don't init window 2 times, # except if force is specified if WindowBase.__instance is not None and not force: return self.initialized = False self._is_desktop = Config.getboolean('kivy', 'desktop') # create a trigger for update/create the window when one of window # property changes self.trigger_create_window = Clock.create_trigger( self.create_window, -1) # Create a trigger for updating the keyboard height self.trigger_keyboard_height = Clock.create_trigger( self._upd_kbd_height, .5) # set the default window parameter according to the configuration if 'borderless' not in kwargs: kwargs['borderless'] = Config.getboolean('graphics', 'borderless') if 'fullscreen' not in kwargs: fullscreen = Config.get('graphics', 'fullscreen') if fullscreen not in ('auto', 'fake'): fullscreen = fullscreen.lower() in ('true', '1', 'yes', 'yup') kwargs['fullscreen'] = fullscreen if 'width' not in kwargs: kwargs['width'] = Config.getint('graphics', 'width') if 'height' not in kwargs: kwargs['height'] = Config.getint('graphics', 'height') if 'rotation' not in kwargs: kwargs['rotation'] = Config.getint('graphics', 'rotation') if 'position' not in kwargs: kwargs['position'] = Config.getdefault('graphics', 'position', 'auto') if 'top' in kwargs: kwargs['position'] = 'custom' kwargs['top'] = kwargs['top'] else: kwargs['top'] = Config.getint('graphics', 'top') if 'left' in kwargs: kwargs['position'] = 'custom' kwargs['left'] = kwargs['left'] else: kwargs['left'] = Config.getint('graphics', 'left') kwargs['_size'] = (kwargs.pop('width'), kwargs.pop('height')) super(WindowBase, self).__init__(**kwargs) # bind all the properties that need to recreate the window self._bind_create_window() self.bind(size=self.trigger_keyboard_height, rotation=self.trigger_keyboard_height) self.bind(softinput_mode=lambda *dt: self.update_viewport(), keyboard_height=lambda *dt: self.update_viewport()) # init privates self._system_keyboard = Keyboard(window=self) self._keyboards = {'system': self._system_keyboard} self._vkeyboard_cls = None self.children = [] self.parent = self # before creating the window import kivy.core.gl # NOQA # configure the window self.create_window() # attach modules + listener event EventLoop.set_window(self) Modules.register_window(self) EventLoop.add_event_listener(self) # manage keyboard(s) self.configure_keyboards() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() # mark as initialized self.initialized = True def _bind_create_window(self): for prop in ('fullscreen', 'borderless', 'position', 'top', 'left', '_size', 'system_size'): self.bind(**{prop: self.trigger_create_window}) def _unbind_create_window(self): for prop in ('fullscreen', 'borderless', 'position', 'top', 'left', '_size', 'system_size'): self.unbind(**{prop: self.trigger_create_window}) def toggle_fullscreen(self): '''Toggle between fullscreen and windowed mode. .. deprecated:: 1.9.0 Use :attr:`fullscreen` instead. ''' pass def maximize(self): '''Maximizes the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: maximize() is not implemented in the current ' 'window provider.') def minimize(self): '''Minimizes the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: minimize() is not implemented in the current ' 'window provider.') def restore(self): '''Restores the size and position of a maximized or minimized window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: restore() is not implemented in the current ' 'window provider.') def hide(self): '''Hides the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: hide() is not implemented in the current ' 'window provider.') def show(self): '''Shows the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: show() is not implemented in the current ' 'window provider.') def close(self): '''Close the window''' pass def create_window(self, *largs): '''Will create the main window and configure it. .. warning:: This method is called automatically at runtime. If you call it, it will recreate a RenderContext and Canvas. This means you'll have a new graphics tree, and the old one will be unusable. This method exist to permit the creation of a new OpenGL context AFTER closing the first one. (Like using runTouchApp() and stopTouchApp()). This method has only been tested in a unittest environment and is not suitable for Applications. Again, don't use this method unless you know exactly what you are doing! ''' # just to be sure, if the trigger is set, and if this method is # manually called, unset the trigger Clock.unschedule(self.create_window) # ensure the window creation will not be called twice if platform in ('android', 'ios'): self._unbind_create_window() if not self.initialized: from kivy.core.gl import init_gl init_gl() # create the render context and canvas, only the first time. from kivy.graphics import RenderContext, Canvas self.render_context = RenderContext() self.canvas = Canvas() self.render_context.add(self.canvas) else: # if we get initialized more than once, then reload opengl state # after the second time. # XXX check how it's working on embed platform. if platform == 'linux' or Window.__class__.__name__ == 'WindowSDL': # on linux, it's safe for just sending a resize. self.dispatch('on_resize', *self.system_size) else: # on other platform, window are recreated, we need to reload. from kivy.graphics.context import get_context get_context().reload() Clock.schedule_once(lambda x: self.canvas.ask_update(), 0) self.dispatch('on_resize', *self.system_size) # ensure the gl viewport is correct self.update_viewport() def on_flip(self): '''Flip between buffers (event)''' self.flip() def flip(self): '''Flip between buffers''' pass def _update_childsize(self, instance, value): self.update_childsize([instance]) def add_widget(self, widget, canvas=None): '''Add a widget to a window''' widget.parent = self self.children.insert(0, widget) canvas = self.canvas.before if canvas == 'before' else \ self.canvas.after if canvas == 'after' else self.canvas canvas.add(widget.canvas) self.update_childsize([widget]) widget.bind(pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def remove_widget(self, widget): '''Remove a widget from a window ''' if not widget in self.children: return self.children.remove(widget) if widget.canvas in self.canvas.children: self.canvas.remove(widget.canvas) elif widget.canvas in self.canvas.after.children: self.canvas.after.remove(widget.canvas) elif widget.canvas in self.canvas.before.children: self.canvas.before.remove(widget.canvas) widget.parent = None widget.unbind(pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def clear(self): '''Clear the window with the background color''' # XXX FIXME use late binding from kivy.graphics.opengl import glClearColor, glClear, \ GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) def set_title(self, title): '''Set the window title. .. versionadded:: 1.0.5 ''' self.title = title def set_icon(self, filename): '''Set the icon of the window. .. versionadded:: 1.0.5 ''' self.icon = filename def to_widget(self, x, y, initial=True, relative=False): return (x, y) def to_window(self, x, y, initial=True, relative=False): return (x, y) def _apply_transform(self, m): return m def get_window_matrix(self, x=0, y=0): m = Matrix() m.translate(x, y, 0) return m def get_root_window(self): return self def get_parent_window(self): return self def get_parent_layout(self): return None def on_draw(self): self.clear() self.render_context.draw() def on_motion(self, etype, me): '''Event called when a Motion Event is received. :Parameters: `etype`: str One of 'begin', 'update', 'end' `me`: :class:`~kivy.input.motionevent.MotionEvent` The Motion Event currently dispatched. ''' if me.is_touch: w, h = self.system_size me.scale_for_screen(w, h, rotation=self._rotation, smode=self.softinput_mode, kheight=self.keyboard_height) if etype == 'begin': self.dispatch('on_touch_down', me) elif etype == 'update': self.dispatch('on_touch_move', me) elif etype == 'end': self.dispatch('on_touch_up', me) FocusBehavior._handle_post_on_touch_up(me) def on_touch_down(self, touch): '''Event called when a touch down event is initiated. .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Event called when a touch event moves (changes location). .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Event called when a touch event is released (terminated). .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_up', touch): return True def on_resize(self, width, height): '''Event called when the window is resized.''' self.update_viewport() def update_viewport(self): from kivy.graphics.opengl import glViewport from kivy.graphics.transformation import Matrix from math import radians w, h = self.system_size smode = self.softinput_mode kheight = self.keyboard_height w2, h2 = w / 2., h / 2. r = radians(self.rotation) x, y = 0, 0 _h = h if smode: y = kheight if smode == 'scale': _h -= kheight # prepare the viewport glViewport(x, y, w, _h) # do projection matrix projection_mat = Matrix() projection_mat.view_clip(0.0, w, 0.0, h, -1.0, 1.0, 0) self.render_context['projection_mat'] = projection_mat # do modelview matrix modelview_mat = Matrix().translate(w2, h2, 0) modelview_mat = modelview_mat.multiply(Matrix().rotate(r, 0, 0, 1)) w, h = self.size w2, h2 = w / 2., h / 2. modelview_mat = modelview_mat.multiply(Matrix().translate(-w2, -h2, 0)) self.render_context['modelview_mat'] = modelview_mat # redraw canvas self.canvas.ask_update() # and update childs self.update_childsize() def update_childsize(self, childs=None): width, height = self.size if childs is None: childs = self.children for w in childs: shw, shh = w.size_hint if shw and shh: w.size = shw * width, shh * height elif shw: w.width = shw * width elif shh: w.height = shh * height for key, value in w.pos_hint.items(): if key == 'x': w.x = value * width elif key == 'right': w.right = value * width elif key == 'y': w.y = value * height elif key == 'top': w.top = value * height elif key == 'center_x': w.center_x = value * width elif key == 'center_y': w.center_y = value * height def screenshot(self, name='screenshot{:04d}.png'): '''Save the actual displayed image in a file ''' i = 0 path = None if name != 'screenshot{:04d}.png': _ext = name.split('.')[-1] name = ''.join((name[:-(len(_ext) + 1)], '{:04d}.', _ext)) while True: i += 1 path = join(getcwd(), name.format(i)) if not exists(path): break return path def on_rotate(self, rotation): '''Event called when the screen has been rotated. ''' pass def on_close(self, *largs): '''Event called when the window is closed''' Modules.unregister_window(self) EventLoop.remove_event_listener(self) def on_request_close(self, *largs, **kwargs): '''Event called before we close the window. If a bound function returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. .. warning:: When the bound function returns True the window will not be closed, so use with care because the user would not be able to close the program, even if the red X is clicked. ''' pass def on_mouse_down(self, x, y, button, modifiers): '''Event called when the mouse is used (pressed/released)''' pass def on_mouse_move(self, x, y, modifiers): '''Event called when the mouse is moved with buttons pressed''' pass def on_mouse_up(self, x, y, button, modifiers): '''Event called when the mouse is moved with buttons pressed''' pass def on_joy_axis(self, stickid, axisid, value): '''Event called when a joystick has a stick or other axis moved .. versionadded:: 1.9.0''' pass def on_joy_hat(self, stickid, hatid, value): '''Event called when a joystick has a hat/dpad moved .. versionadded:: 1.9.0''' pass def on_joy_ball(self, stickid, ballid, value): '''Event called when a joystick has a ball moved .. versionadded:: 1.9.0''' pass def on_joy_button_down(self, stickid, buttonid): '''Event called when a joystick has a button pressed .. versionadded:: 1.9.0''' pass def on_joy_button_up(self, stickid, buttonid): '''Event called when a joystick has a button released .. versionadded:: 1.9.0''' pass def on_keyboard(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when keyboard is used. .. warning:: Some providers may omit `scancode`, `codepoint` and/or `modifier`! ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") # Quit if user presses ESC or the typical OSX shortcuts CMD+q or CMD+w # TODO If just CMD+w is pressed, only the window should be closed. is_osx = platform == 'darwin' if WindowBase.on_keyboard.exit_on_escape: if key == 27 or all([is_osx, key in [113, 119], modifier == 1024]): if not self.dispatch('on_request_close', source='keyboard'): stopTouchApp() self.close() return True if Config: on_keyboard.exit_on_escape = Config.getboolean('kivy', 'exit_on_escape') def __exit(section, name, value): WindowBase.__dict__['on_keyboard'].exit_on_escape = \ Config.getboolean('kivy', 'exit_on_escape') Config.add_callback(__exit, 'kivy', 'exit_on_escape') def on_key_down(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is down (same arguments as on_keyboard)''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") def on_key_up(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is released (same arguments as on_keyboard) ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") def on_textinput(self, text): '''Event called whem text: i.e. alpha numeric non control keys or set of keys is entered. As it is not gaurenteed whether we get one character or multiple ones, this event supports handling multiple characters. ..versionadded:: 1.9.0 ''' pass def on_dropfile(self, filename): '''Event called when a file is dropped on the application. .. warning:: This event currently works with sdl2 window provider, on pygame window provider and MacOSX with a patched version of pygame. This event is left in place for further evolution (ios, android etc.) .. versionadded:: 1.2.0 ''' pass @reify def dpi(self): '''Return the DPI of the screen. If the implementation doesn't support any DPI lookup, it will just return 96. .. warning:: This value is not cross-platform. Use :attr:`kivy.base.EventLoop.dpi` instead. ''' return 96. def configure_keyboards(self): # Configure how to provide keyboards (virtual or not) # register system keyboard to listening keys from window sk = self._system_keyboard self.bind(on_key_down=sk._on_window_key_down, on_key_up=sk._on_window_key_up, on_textinput=sk._on_window_textinput) # use the device's real keyboard self.use_syskeyboard = True # use the device's real keyboard self.allow_vkeyboard = False # one single vkeyboard shared between all widgets self.single_vkeyboard = True # the single vkeyboard is always sitting at the same position self.docked_vkeyboard = False # now read the configuration mode = Config.get('kivy', 'keyboard_mode') if mode not in ('', 'system', 'dock', 'multi', 'systemanddock', 'systemandmulti'): Logger.critical('Window: unknown keyboard mode %r' % mode) # adapt mode according to the configuration if mode == 'system': self.use_syskeyboard = True self.allow_vkeyboard = False self.single_vkeyboard = True self.docked_vkeyboard = False elif mode == 'dock': self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'multi': self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False elif mode == 'systemanddock': self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'systemandmulti': self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False Logger.info( 'Window: virtual keyboard %sallowed, %s, %s' % ('' if self.allow_vkeyboard else 'not ', 'single mode' if self.single_vkeyboard else 'multiuser mode', 'docked' if self.docked_vkeyboard else 'not docked')) def set_vkeyboard_class(self, cls): '''.. versionadded:: 1.0.8 Set the VKeyboard class to use. If set to None, it will use the :class:`kivy.uix.vkeyboard.VKeyboard`. ''' self._vkeyboard_cls = cls def release_all_keyboards(self): '''.. versionadded:: 1.0.8 This will ensure that no virtual keyboard / system keyboard is requested. All instances will be closed. ''' for key in list(self._keyboards.keys())[:]: keyboard = self._keyboards[key] if keyboard: keyboard.release() def request_keyboard(self, callback, target, input_type='text'): '''.. versionadded:: 1.0.4 Internal widget method to request the keyboard. This method is rarely required by the end-user as it is handled automatically by the :class:`~kivy.uix.textinput.TextInput`. We expose it in case you want to handle the keyboard manually for unique input scenarios. A widget can request the keyboard, indicating a callback to call when the keyboard is released (or taken by another widget). :Parameters: `callback`: func Callback that will be called when the keyboard is closed. This can be because somebody else requested the keyboard or the user closed it. `target`: Widget Attach the keyboard to the specified `target`. This should be the widget that requested the keyboard. Ensure you have a different target attached to each keyboard if you're working in a multi user mode. .. versionadded:: 1.0.8 `input_type`: string Choose the type of soft keyboard to request. Can be one of 'text', 'number', 'url', 'mail', 'datetime', 'tel', 'address'. .. note:: `input_type` is currently only honored on mobile devices. .. versionadded:: 1.8.0 :Return: An instance of :class:`Keyboard` containing the callback, target, and if the configuration allows it, a :class:`~kivy.uix.vkeyboard.VKeyboard` instance attached as a *.widget* property. .. note:: The behavior of this function is heavily influenced by the current `keyboard_mode`. Please see the Config's :ref:`configuration tokens <configuration-tokens>` section for more information. ''' # release any previous keyboard attached. self.release_keyboard(target) # if we can use virtual vkeyboard, activate it. if self.allow_vkeyboard: keyboard = None # late import global VKeyboard if VKeyboard is None and self._vkeyboard_cls is None: from kivy.uix.vkeyboard import VKeyboard self._vkeyboard_cls = VKeyboard # if the keyboard doesn't exist, create it. key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: vkeyboard = self._vkeyboard_cls() keyboard = Keyboard(widget=vkeyboard, window=self) vkeyboard.bind(on_key_down=keyboard._on_vkeyboard_key_down, on_key_up=keyboard._on_vkeyboard_key_up, on_textinput=keyboard._on_vkeyboard_textinput) self._keyboards[key] = keyboard else: keyboard = self._keyboards[key] # configure vkeyboard keyboard.target = keyboard.widget.target = target keyboard.callback = keyboard.widget.callback = callback # add to the window self.add_widget(keyboard.widget) # only after add, do dock mode keyboard.widget.docked = self.docked_vkeyboard keyboard.widget.setup_mode() else: # system keyboard, just register the callback. keyboard = self._system_keyboard keyboard.callback = callback keyboard.target = target # use system (hardware) keyboard according to flag if self.allow_vkeyboard and self.use_syskeyboard: self.unbind(on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up, on_textinput=keyboard._on_window_textinput) self.bind(on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up, on_textinput=keyboard._on_window_textinput) return keyboard def release_keyboard(self, target=None): '''.. versionadded:: 1.0.4 Internal method for the widget to release the real-keyboard. Check :meth:`request_keyboard` to understand how it works. ''' if self.allow_vkeyboard: key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: return keyboard = self._keyboards[key] callback = keyboard.callback if callback: keyboard.callback = None callback() keyboard.target = None self.remove_widget(keyboard.widget) if key != 'single' and key in self._keyboards: del self._keyboards[key] elif self._system_keyboard.callback: # this way will prevent possible recursion. callback = self._system_keyboard.callback self._system_keyboard.callback = None callback() return True
class ShinobiViewer(App): fullscreen = BooleanProperty(False) def on_fullscreen(self, instance, value): if value: Window.fullscreen = 'auto' else: Window.fullscreen = False def build(self): self.settings_cls = SettingsWithSidebar self.available_layouts = [ os.path.splitext(os.path.basename(l))[0] for l in glob("layouts/*.kv") ] Window.bind(on_keyboard=self.on_keyboard) # bind our handler monitors = self.config['monitors'] self.root = BoxLayout() self.add_monitors() return self.root def add_monitors(self): monitors = self.config['monitors'].values() if not monitors: self.root.add_widget(Label(text='No monitors')) return self.root config = self.config['shinobi'] monitor_urls = [ '/%s/monitor/%s/%s' % (config['apiKey'], config['groupKey'], mid) for mid in monitors ] server_url = config['server'].rstrip('/') if config['layout'] == 'auto': layout = self.auto_layout(len(monitors)) else: layout = Builder.load_file('layouts/%s.kv' % config['layout']) self.monitor_widgets = [ widget for widget in layout.walk(restrict=True) if isinstance(widget, ShinobiMonitor) ] for url, monitor in zip(monitor_urls, self.monitor_widgets): monitor.serverURL = server_url monitor.monitorPath = url monitor.start() self.root.add_widget(layout) return self.root def fetch_monitors(self): shinobi_monitors_url = '/'.join([ self.config['shinobi']['server'].rstrip('/'), self.config['shinobi']['apikey'], 'monitor', self.config['shinobi']['groupkey'], ]) try: response = requests.get(shinobi_monitors_url, timeout=60) except: return [] finally: for monitor in response.json(): details = json.loads(monitor['details']) yield { 'mid': monitor['mid'], 'name': monitor['name'], 'groups': details['groups'] } def build_settings(self, settings): settings.add_json_panel('Connection', self.config, data=json.dumps([ { "type": "string", "title": "Server URL", "desc": "URL of shinobi server", "section": "shinobi", "key": "server" }, { "type": "string", "title": "API Key", "desc": "Server API key", "section": "shinobi", "key": "apikey" }, { "type": "string", "title": "Group Key", "desc": "User API key", "section": "shinobi", "key": "groupkey" }, { "type": "title", "title": "Monitors" }, { "type": "options", "title": "Layout", "desc": "Monitors layout on screen", "section": "shinobi", "key": "layout", "options": ["auto"] + self.available_layouts }, ])) monitors_panel = MonitorSettingsPanel( title="Monitors", settings=settings, config=self.config, fetch_func=self.fetch_monitors, ) settings.interface.add_panel(monitors_panel, 'Monitors', monitors_panel.uid) def build_config(self, config): config.setdefaults('shinobi', { 'layout': 'auto', 'server': '', 'apiKey': '', 'groupKey': '' }) config.adddefaultsection('monitors') def on_keyboard(self, window, key, scancode, codepoint, modifier): if scancode == 68: # F11 self.fullscreen = not self.fullscreen if scancode == 69: # F12 self.open_settings() def auto_layout(self, qtd): cols = math.ceil(math.sqrt(qtd)) layout = GridLayout(cols=cols) for i in range(qtd): layout.add_widget(ShinobiMonitor()) return layout def start_stop_monitors(self, start): for monitor in getattr(self, 'monitor_widgets', []): if start: monitor.start() else: monitor.stop() def open_settings(self, *args, **kwargs): self.start_stop_monitors(False) self.root.clear_widgets() return super().open_settings(*args, **kwargs) def close_settings(self, *args, **kwargs): self.start_stop_monitors(True) self.add_monitors() return super().close_settings(*args, **kwargs) def on_pause(self): self.start_stop_monitors(False) return True def on_resume(self): self.start_stop_monitors(True)
class DataTable(GridLayout): """This is a compound widget designed to display a dictionary of data as a nice table. The dictionary should have the column headers as keys, and then the associated value is a list of data for that column. You may have lists of different lengths, but the columns will fill from the top down; therefore, include blank strings as placeholders for any empty cells. Note that since the column headers are dict keys, you must have unique column names. Sorry...""" data = DictProperty({}) ncol = NumericProperty(0) nrow = NumericProperty(0) editable = BooleanProperty(False) header_col = StringProperty('') def __init__(self, data={}, editable=False, header_column='', header_row=[], **kw): super(DataTable, self).__init__(**kw) self.bind(height=self.setter('minimum_height')) self.data = data self.ncol = len(data) self.editable = editable self.header_col = header_column self.header_row = header_row celltype = EditableCell if self.editable else StaticCell self.nrow = max([len(data[x]) for x in data]) self.cells = {} for key in self.header_row: cell_id = str(key) + '_head' cell = ColHeader(text=str(key), data_table=self, id=cell_id, size_hint_y=None, height=100) self.cells[cell_id] = cell self.add_widget(cell) for i in xrange(self.nrow): get = itemgetter(i) for key in self.header_row: cell_id = str(key) + '_' + str(i) if i <= len(self.data[key]): text = get(self.data[key]) else: text = '' self.data[key].append('') if key == self.header_col: self.cells[cell_id] = RowHeader(text=str(text), data_table=self, id=cell_id, initial_type=type(text)) else: self.cells[cell_id] = celltype(text=str(text), data_table=self, id=cell_id, initial_type=type(text)) self.add_widget(self.cells[cell_id]) def data_update(self, cell_id, value): """This will try to convert the value to the initial type of the data. If that fails, it'll just be a string. The initial type won't change, however.""" key, idx = cell_id.split('_') try: val = self.cells[cell_id].initial_type(value) except ValueError: val = value self.data[key][int(idx)] = val def sort_by(self, colname): column_to_order = enumerate(self.data[colname]) sort_order = map(itemgetter(0), sorted(column_to_order, key=itemgetter(1))) for key in self.data: col = self.data[key] self.data[key] = [col[x] for x in sort_order] self.cells[str(key) + '_head'].background_color = (1, 1, 1, 1) for i in xrange(self.nrow): self.cells[str(key) + '_' + str(i)].text = str( self.data[key][i]) self.cells[colname + '_head'].background_color = (0, 1, 0, 1)
class BaseButton(ThemableBehavior, ButtonBehavior, SpecificBackgroundColorBehavior, AnchorLayout): ''' Abstract base class for all MD buttons. This class handles the button's colors (disabled/down colors handled in children classes as those depend on type of button) as well as the disabled state. ''' _md_bg_color_down = ListProperty(None, allownone=True) _md_bg_color_disabled = ListProperty(None, allownone=True) _current_button_color = ListProperty([0., 0., 0., 0.]) theme_text_color = OptionProperty(None, allownone=True, options=[ 'Primary', 'Secondary', 'Hint', 'Error', 'Custom', 'ContrastParentBackground' ]) text_color = ListProperty(None, allownone=True) opposite_colors = BooleanProperty(False) def __init__(self, **kwargs): super(BaseButton, self).__init__(**kwargs) Clock.schedule_once(self._finish_init) def _finish_init(self, dt): self._update_color() def on_md_bg_color(self, instance, value): self._update_color() def _update_color(self): if not self.disabled: self._current_button_color = self.md_bg_color else: self._current_button_color = self.md_bg_color_disabled def _call_get_bg_color_down(self): return self._get_md_bg_color_down() def _get_md_bg_color_down(self): if self._md_bg_color_down: return self._md_bg_color_down else: raise NotImplementedError def _set_md_bg_color_down(self, value): self._md_bg_color_down = value md_bg_color_down = AliasProperty(_call_get_bg_color_down, _set_md_bg_color_down) def _call_get_bg_color_disabled(self): return self._get_md_bg_color_disabled() def _get_md_bg_color_disabled(self): if self._md_bg_color_disabled: return self._md_bg_color_disabled else: raise NotImplementedError def _set_md_bg_color_disabled(self, value): self._md_bg_color_disabled = value md_bg_color_disabled = AliasProperty(_call_get_bg_color_disabled, _set_md_bg_color_disabled) def on_disabled(self, instance, value): if value: self._current_button_color = self.md_bg_color_disabled else: self._current_button_color = self.md_bg_color super(BaseButton, self).on_disabled(instance, value)
class VideoPlayer(GridLayout): '''VideoPlayer class. See module documentation for more information. ''' source = StringProperty('') '''Source of the video to read. :attr:`source` is a :class:`~kivy.properties.StringProperty` and defaults to ''. .. versionchanged:: 1.4.0 ''' thumbnail = StringProperty('') '''Thumbnail of the video to show. If None, VideoPlayer will try to find the thumbnail from the :attr:`source` + '.png'. :attr:`thumbnail` a :class:`~kivy.properties.StringProperty` and defaults to ''. .. versionchanged:: 1.4.0 ''' duration = NumericProperty(-1) '''Duration of the video. The duration defaults to -1 and is set to the real duration when the video is loaded. :attr:`duration` is a :class:`~kivy.properties.NumericProperty` and defaults to -1. ''' position = NumericProperty(0) '''Position of the video between 0 and :attr:`duration`. The position defaults to -1 and is set to the real position when the video is loaded. :attr:`position` is a :class:`~kivy.properties.NumericProperty` and defaults to -1. ''' volume = NumericProperty(1.0) '''Volume of the video in the range 0-1. 1 means full volume and 0 means mute. :attr:`volume` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' state = OptionProperty('stop', options=('play', 'pause', 'stop')) '''String, indicates whether to play, pause, or stop the video:: # start playing the video at creation video = VideoPlayer(source='movie.mkv', state='play') # create the video, and start later video = VideoPlayer(source='movie.mkv') # and later video.state = 'play' :attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults to 'stop'. ''' play = BooleanProperty(False, deprecated=True) ''' .. deprecated:: 1.4.0 Use :attr:`state` instead. Boolean, indicates whether the video is playing or not. You can start/stop the video by setting this property:: # start playing the video at creation video = VideoPlayer(source='movie.mkv', play=True) # create the video, and start later video = VideoPlayer(source='movie.mkv') # and later video.play = True :attr:`play` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' image_overlay_play = StringProperty( 'atlas://data/images/defaulttheme/player-play-overlay') '''Image filename used to show a "play" overlay when the video has not yet started. :attr:`image_overlay_play` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/player-play-overlay'. ''' image_loading = StringProperty('data/images/image-loading.gif') '''Image filename used when the video is loading. :attr:`image_loading` is a :class:`~kivy.properties.StringProperty` and defaults to 'data/images/image-loading.gif'. ''' image_play = StringProperty( 'atlas://data/images/defaulttheme/media-playback-start') '''Image filename used for the "Play" button. :attr:`image_play` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/media-playback-start'. ''' image_stop = StringProperty( 'atlas://data/images/defaulttheme/media-playback-stop') '''Image filename used for the "Stop" button. :attr:`image_stop` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/media-playback-stop'. ''' image_pause = StringProperty( 'atlas://data/images/defaulttheme/media-playback-pause') '''Image filename used for the "Pause" button. :attr:`image_pause` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/media-playback-pause'. ''' image_volumehigh = StringProperty( 'atlas://data/images/defaulttheme/audio-volume-high') '''Image filename used for the volume icon when the volume is high. :attr:`image_volumehigh` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/audio-volume-high'. ''' image_volumemedium = StringProperty( 'atlas://data/images/defaulttheme/audio-volume-medium') '''Image filename used for the volume icon when the volume is medium. :attr:`image_volumemedium` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/audio-volume-medium'. ''' image_volumelow = StringProperty( 'atlas://data/images/defaulttheme/audio-volume-low') '''Image filename used for the volume icon when the volume is low. :attr:`image_volumelow` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/audio-volume-low'. ''' image_volumemuted = StringProperty( 'atlas://data/images/defaulttheme/audio-volume-muted') '''Image filename used for the volume icon when the volume is muted. :attr:`image_volumemuted` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/audio-volume-muted'. ''' annotations = StringProperty('') '''If set, it will be used for reading annotations box. :attr:`annotations` is a :class:`~kivy.properties.StringProperty` and defaults to ''. ''' fullscreen = BooleanProperty(False) '''Switch to fullscreen view. This should be used with care. When activated, the widget will remove itself from its parent, remove all children from the window and will add itself to it. When fullscreen is unset, all the previous children are restored and the widget is restored to its previous parent. .. warning:: The re-add operation doesn't care about the index position of its children within the parent. :attr:`fullscreen` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' allow_fullscreen = BooleanProperty(True) '''By default, you can double-tap on the video to make it fullscreen. Set this property to False to prevent this behavior. :attr:`allow_fullscreen` is a :class:`~kivy.properties.BooleanProperty` defaults to True. ''' options = DictProperty({}) '''Optional parameters can be passed to a :class:`~kivy.uix.video.Video` instance with this property. :attr:`options` a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' # internals container = ObjectProperty(None) _video_load_ev = None def __init__(self, **kwargs): self._video = None self._image = None self._annotations = '' self._annotations_labels = [] super(VideoPlayer, self).__init__(**kwargs) self._load_thumbnail() self._load_annotations() if self.source: self._trigger_video_load() def _trigger_video_load(self, *largs): ev = self._video_load_ev if ev is None: ev = self._video_load_ev = Clock.schedule_once(self._do_video_load, -1) ev() def on_source(self, instance, value): # we got a value, try to see if we have an image for it self._load_thumbnail() self._load_annotations() if self._video is not None: self._video.unload() self._video = None if value: self._trigger_video_load() def on_image_overlay_play(self, instance, value): self._image.image_overlay_play = value def on_image_loading(self, instance, value): self._image.image_loading = value def _load_thumbnail(self): if not self.container: return self.container.clear_widgets() # get the source, remove extension, and use png thumbnail = self.thumbnail if not thumbnail: filename = self.source.rsplit('.', 1) thumbnail = filename[0] + '.png' if not exists(thumbnail): thumbnail = '' self._image = VideoPlayerPreview(source=thumbnail, video=self) self.container.add_widget(self._image) def _load_annotations(self): if not self.container: return self._annotations_labels = [] annotations = self.annotations if not annotations: filename = self.source.rsplit('.', 1) annotations = filename[0] + '.jsa' if exists(annotations): with open(annotations, 'r') as fd: self._annotations = load(fd) if self._annotations: for ann in self._annotations: self._annotations_labels.append( VideoPlayerAnnotation(annotation=ann)) def on_state(self, instance, value): if self._video is not None: self._video.state = value def _set_state(self, instance, value): self.state = value def _do_video_load(self, *largs): self._video = Video(source=self.source, state=self.state, volume=self.volume, pos_hint={'x': 0, 'y': 0}, **self.options) self._video.bind(texture=self._play_started, duration=self.setter('duration'), position=self.setter('position'), volume=self.setter('volume'), state=self._set_state) def on_play(self, instance, value): value = 'play' if value else 'stop' return self.on_state(instance, value) def on_volume(self, instance, value): if not self._video: return self._video.volume = value def on_position(self, instance, value): labels = self._annotations_labels if not labels: return for label in labels: start = label.start duration = label.duration if start > value or (start + duration) < value: if label.parent: label.parent.remove_widget(label) elif label.parent is None: self.container.add_widget(label) def seek(self, percent, precise=True): '''Change the position to a percentage of duration. :Parameters: `percent`: float or int Position to seek, must be between 0-1. `precise`: bool, defaults to True Precise seeking is slower, but seeks to exact requested percent. .. warning:: Calling seek() before the video is loaded has no effect. .. versionadded:: 1.2.0 .. versionchanged:: 1.10.1 The `precise` keyword argument has been added. ''' if not self._video: return self._video.seek(percent, precise=precise) def _play_started(self, instance, value): self.container.clear_widgets() self.container.add_widget(self._video) def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return False if touch.is_double_tap and self.allow_fullscreen: self.fullscreen = not self.fullscreen return True return super(VideoPlayer, self).on_touch_down(touch) def on_fullscreen(self, instance, value): window = self.get_parent_window() if not window: Logger.warning('VideoPlayer: Cannot switch to fullscreen, ' 'window not found.') if value: self.fullscreen = False return if not self.parent: Logger.warning('VideoPlayer: Cannot switch to fullscreen, ' 'no parent.') if value: self.fullscreen = False return if value: self._fullscreen_state = state = { 'parent': self.parent, 'pos': self.pos, 'size': self.size, 'pos_hint': self.pos_hint, 'size_hint': self.size_hint, 'window_children': window.children[:]} # remove all window children for child in window.children[:]: window.remove_widget(child) # put the video in fullscreen if state['parent'] is not window: state['parent'].remove_widget(self) window.add_widget(self) # ensure the video widget is in 0, 0, and the size will be # readjusted self.pos = (0, 0) self.size = (100, 100) self.pos_hint = {} self.size_hint = (1, 1) else: state = self._fullscreen_state window.remove_widget(self) for child in state['window_children']: window.add_widget(child) self.pos_hint = state['pos_hint'] self.size_hint = state['size_hint'] self.pos = state['pos'] self.size = state['size'] if state['parent'] is not window: state['parent'].add_widget(self)
class SendScreen(Screen): send_button_text = StringProperty('send') send_button_disabled = BooleanProperty(False) has_code = BooleanProperty(False) code = StringProperty('…') file_name = StringProperty('…') file_size = StringProperty('…') def on_pre_enter(self): """ Reset the labels and buttons and init a magic wormhole instance. This method is called just before the user enters this screen. """ self.send_button_disabled = True self.send_button_text = 'waiting for code' self.has_code = False self.code = '…' self.file_path = None self.file_name = '…' self.file_size = '…' self.wormhole = Wormhole() def update_code(code): self.has_code = True self.code = code self.send_button_disabled = False self.send_button_text = 'send' Clipboard.copy(code) deferred = self.wormhole.generate_code() deferred.addCallbacks(update_code, ErrorPopup.show) def set_file(self, path): """ Set the file to be sent down the wormhole. This method is called when the the user selects the file to send using the file chooser. It is also called directly by the App instance when the app has been started or resumed via a send file intent on Android. """ try: path = os.path.normpath(path) assert os.path.exists(path) and os.path.isfile(path) except: ErrorPopup.show( ('There is something wrong about the file you chose. ' 'One possible reason is an issue with some Androids ' 'where a file cannot be directly selected from the ' '"Downloads" section and instead you have to reach it ' 'some other way, e.g. "Phone" -> "Downloads".')) self.file_path = None self.file_name = '…' self.file_size = '…' else: self.file_path = path self.file_name = os.path.basename(self.file_path) self.file_size = humanize.naturalsize( os.stat(self.file_path).st_size) def open_file_chooser(self): """ Open a file chooser so that the user can select a file to send down the wormhole. On Android, this could be preceded by asking for permissions. This method is called when the user releases the "choose file" button. """ def handle_selection(selection): if selection: self.set_file(selection[0]) def show_error(): ErrorPopup.show( ('You cannot send a file if the app cannot access it.')) @ensure_storage_perms(show_error) def open_file_chooser(): filechooser.open_file(title='Choose a file to send', on_selection=handle_selection) open_file_chooser() def send(self): """ Send the selected file down the wormhole. This method is called when the user releases the send button. """ if not self.file_path: return ErrorPopup.show('Please choose a file to send.') def exchange_keys(): self.send_button_disabled = True self.send_button_text = 'exchanging keys' deferred = self.wormhole.exchange_keys(timeout=600) deferred.addCallbacks(send_file, ErrorPopup.show) def send_file(verifier): self.send_button_disabled = True self.send_button_text = 'sending file' deferred = self.wormhole.send_file(self.file_path) deferred.addCallbacks(show_done, ErrorPopup.show) def show_done(hex_digest): self.send_button_disabled = True self.send_button_text = 'done' exchange_keys() def on_leave(self): """ Close the magic wormhole instance, if this is still open. This method is called when the user leaves this screen. """ self.wormhole.close()
class KbDesbloqueio(ModalView): txt_cabecalho = StringProperty( 'DIGITE CÓDIGO DE ACESSO PARA CANCELAR ALARME') txt_acesso_usu_view = StringProperty() txt_acesso_sys = '' acesso_confirmado = BooleanProperty(False) conta_tentativas = NumericProperty(0) acesso = JsonStore('json/acesso.json') if len(acesso) <= 1: tst_cad_acess = False txt_cabecalho = 'DIGITE CÓDIGO DE ACESSO PARA CANCELAR ALARME' def kb_keypress(self, keypressed): st_sys = JsonStore('json/status.json') if self.conta_tentativas <= 3: self.txt_cabecalho = 'DIGITE CÓDIGO DE ACESSO PARA CANCELAR ALARME' if len(self.txt_acesso_sys) != 6: self.txt_acesso_sys = self.txt_acesso_sys + keypressed self.txt_acesso_usu_view = self.txt_acesso_usu_view + '*' if len(self.txt_acesso_sys) == 6: if self.consulta_acesso(self.txt_acesso_sys): self.acesso_confirmado = True self.txt_cabecalho = 'PRESSIONE "CONFIRMA" PARA CANCELAR ALARME' self.conta_tentativas = 0 if self.conta_tentativas <= 3 and self.acesso_confirmado: st_sys.put('alrm', st=1) self.dismiss() else: self.acesso_confirmado = False self.txt_cabecalho = 'CÓDIGO INVÁLIDO' self.txt_acesso_usu_view = '' self.txt_acesso_sys = '' self.conta_tentativas += 1 st_sys.put('alrm', st=2) if self.conta_tentativas > 3: st_sys.put('alrm', st=3) self.dismiss() else: self.dismiss() def consulta_acesso(self, codigo_acesso): if self.acesso.exists(codigo_acesso): self.txt_cabecalho = 'PRESSIONE "CONFIRMA" PARA CACELAR ALARME' return True else: self.txt_cabecalho = 'CÓDIGO INVÁLIDO' return False def ajusta_st_alr_desbloqueio(self): st_sys = JsonStore('json/status.json') print st_sys.get( 'alrm')['st'], self.acesso_confirmado, self.conta_tentativas if st_sys.get('alrm')['st'] == 2 and self.conta_tentativas > 3: st_sys.put('alrm', st=3) self.dismiss() elif st_sys.get( 'alrm' )['st'] == 1 and self.acesso_confirmado and self.conta_tentativas <= 3: st_sys.put('alrm', st=1) self.dismiss() elif st_sys.get( 'alrm' )['st'] == 2 and self.acesso_confirmado and self.conta_tentativas <= 3: st_sys.put('alrm', st=1) elif st_sys.get( 'alrm' )['st'] == 2 and not self.acesso_confirmado and self.conta_tentativas <= 3: st_sys.put('alrm', st=2)
class Collection(EventDispatcher): games = ListProperty([]) lazy_games = ListProperty([]) name = StringProperty('Collection') defaultdir = StringProperty('./games/unsaved') lazy_loaded = BooleanProperty(False) finished_loading = BooleanProperty(False) def __init__(self, *args, **kwargs): if platform() == 'android': self.defaultdir = '/sdcard/noGo/collections/unsaved' super(Collection, self).__init__(*args, **kwargs) def __str__(self): return 'SGF collection {0} with {1} games'.format( self.name, self.number_of_games()) def __repr__(self): return self.__str__() def remove_sgf(self, sgf): self.finish_lazy_loading() if sgf in self.games: self.games.remove(sgf) def get_default_dir(self): return '.' + '/' + self.name def number_of_games(self): if not self.finished_loading: return len(self.lazy_games) else: return len(self.games) def lazy_from_list(self, l): name, defaultdir, games = l self.name = name self.defaultdir = defaultdir self.lazy_games = games self.lazy_loaded = True self.finished_loading = False return self def finish_lazy_loading(self): print 'Finishing lazy loading' if not self.finished_loading and self.lazy_loaded: print 'need to load', len(self.lazy_games) for game in self.lazy_games: print 'currently doing', game try: colsgf = CollectionSgf(collection=self).load(game) self.games.append(colsgf) except IOError: print '(lazy) Tried to load sgf that doesn\'t seem to exist. Skipping.' print 'game was', game self.lazy_games = [] self.finished_loading = True def from_list(self, l): name, defaultdir, games = l self.name = name self.defaultdir = defaultdir for game in games: try: print 'finish loading', game colsgf = CollectionSgf(collection=self).load(game) self.games.append(colsgf) except IOError: print 'Tried to load sgf that doesn\'t seem to exist. Skipping.' print 'game was', game #self.games = map(lambda j: CollectionSgf(collection=self).load(j),games) return self def as_list(self): self.finish_lazy_loading() return [ self.name, self.defaultdir, map(lambda j: j.get_filen(), self.games) ] def serialise(self): self.finish_lazy_loading() return json.dumps([SERIALISATION_VERSION, self.as_list()]) def save(self): self.finish_lazy_loading() if platform() == 'android': filen = '/sdcard/noGo/' + self.name + '.json' else: filen = '.' + '/collections/' + self.name + '.json' with open(filen, 'w') as fileh: fileh.write(self.serialise()) return filen def get_filen(self): filen = '.' + '/collections/' + self.name + '.json' return filen def from_file(self, filen): # print 'Trying to load collection from',filen with open(filen, 'r') as fileh: jsonstr = fileh.read() #print 'File contents are',jsonstr version, selflist = json.loads(jsonstr) selflist = jsonconvert(selflist) return self.lazy_from_list(selflist) def add_game(self, can_change_name=True): self.finish_lazy_loading() game = CollectionSgf(collection=self, can_change_name=can_change_name) game.filen = game.get_default_filen() + '.sgf' self.games.append(game) self.save() return game def random_sgf(self): self.finish_lazy_loading() return random.choice(self.games)
class ButtonBase(Button, Navigation): """Button widget that includes theme options and a variety of small additions over a basic button.""" warn = BooleanProperty(False) target_background = ListProperty() target_text = ListProperty() background_animation = ObjectProperty() text_animation = ObjectProperty() last_disabled = False menu = BooleanProperty(False) toggle = BooleanProperty(False) button_update = BooleanProperty() def __init__(self, **kwargs): self.background_animation = Animation() self.text_animation = Animation() app = App.get_running_app() self.background_color = app.theme.button_up self.target_background = self.background_color self.color = app.theme.button_text self.target_text = self.color super(ButtonBase, self).__init__(**kwargs) def on_button_update(self, *_): Clock.schedule_once(lambda x: self.set_color()) def set_color(self, instant=False): app = App.get_running_app() if self.disabled: self.set_text(app.theme.button_disabled_text, instant=instant) self.set_background(app.theme.button_disabled, instant=instant) else: self.set_text(app.theme.button_text, instant=instant) if self.menu: if self.state == 'down': self.set_background(app.theme.button_menu_down, instant=True) else: self.set_background(app.theme.button_menu_up, instant=instant) elif self.toggle: if self.state == 'down': self.set_background(app.theme.button_toggle_true, instant=instant) else: self.set_background(app.theme.button_toggle_false, instant=instant) elif self.warn: if self.state == 'down': self.set_background(app.theme.button_warn_down, instant=True) else: self.set_background(app.theme.button_warn_up, instant=instant) else: if self.state == 'down': self.set_background(app.theme.button_down, instant=True) else: self.set_background(app.theme.button_up, instant=instant) def on_disabled(self, *_): self.set_color() def on_menu(self, *_): self.set_color(instant=True) def on_toggle(self, *_): self.set_color(instant=True) def on_warn(self, *_): self.set_color(instant=True) def on_state(self, *_): self.set_color() def set_background(self, color, instant=False): if self.target_background == color: return app = App.get_running_app() self.background_animation.stop(self) if app.animations and not instant: self.background_animation = Animation( background_color=color, duration=app.animation_length) self.background_animation.start(self) else: self.background_color = color self.target_background = color def set_text(self, color, instant=False): if self.target_text == color: return app = App.get_running_app() self.text_animation.stop(self) if app.animations and not instant: self.text_animation = Animation(color=color, duration=app.animation_length) self.text_animation.start(self) else: self.color = color self.target_text = color
class InstallerApp(App): status = StringProperty( 'By hitting the [i]Install[/i] button you agree to delete all ' f'contents of [b]{LOCAL_REPO_PATH}[/b] if it exists.') install_progress = NumericProperty() error = BooleanProperty(False) progress_items = OrderedDict({ 'Discover local repository': None, 'Clean the installation directory': None, 'Create the installation directory': None, 'Clone the remote distribution repository': None, 'Fetch the local repository': None, 'Reset the local repository': None, 'Add a shortcut': None, 'Start the app': None }) PROGRESS_MESSAGES = [ 'Using a magnifying glass to find the local repository..', 'Training the installation directory..', 'Creativiting the installation directory..', 'Trying to bait the repository down off the cloud..', 'Trying to get an attention from the remote origin..', 'Hardly trying to reset the local repository..', 'Creating a lovely shortcut..', 'Waking up XtremeUpdater..' ] def __init__(self, *args, **kw): super().__init__(*args, **kw) self.Theme = Theme # animate window def on_frame(__): _height = Window.height Window.size = Window.width, 1 Animation(size=[Window.width, _height], d=1, t='out_expo').start(Window) Clock.schedule_once(on_frame) def install(self): self.error = False self.root.ids.install_btn.disabled = True self.root.ids.progressbar.normal = 0 Animation(color=Theme.prim, d=.5).start(self.root.ids.progressbar) for key in self.progress_items.keys(): self.progress_items[key] = None start_new(self._install, ()) def _install(self): success = True keys = tuple(self.progress_items.keys()) for index, ret in enumerate(Installer.full_install()): _last = index == len(keys) - 1 # white text if ret and not _last: self.progress_items[keys[index + 1]] = -1 self.status = self.PROGRESS_MESSAGES[index + 1] # colored text self.progress_items[keys[index]] = ret # reload todo-list self.root.ids.todo.items = self.progress_items self.root.ids.todo.reload() # error if not ret: success = False self.root.ids.install_btn.disabled = False self.error = True self.status = ("I'm sorry.. I was not a good boy this time. " 'An error occured, please contact our support.') break # Animate progressbar Animation(color=((not success) * .7 + .3, success * .7 + .3, .3, 1), d=.5).start(self.root.ids.progressbar) self.root.ids.progressbar.normal = 1 if success: self.status = 'You can now use [i]XtremeUpdater[/i]. Nice!' Clock.schedule_once(self.stop, 5)
class NormalDropDown(DropDown): """Dropdown menu class with some nice animations.""" show_percent = NumericProperty(1) invert = BooleanProperty(False) basic_animation = BooleanProperty(False) def open(self, *args, **kwargs): if self.parent: self.dismiss() return app = App.get_running_app() super(NormalDropDown, self).open(*args, **kwargs) if app.animations: if self.basic_animation: #Dont do fancy child opacity animation self.opacity = 0 self.show_percent = 1 anim = Animation(opacity=1, duration=app.animation_length) anim.start(self) else: #determine if we opened up or down attach_to_window = self.attach_to.to_window( *self.attach_to.pos) if attach_to_window[1] > self.pos[1]: self.invert = True children = reversed(self.container.children) else: self.invert = False children = self.container.children #Animate background self.opacity = 1 self.show_percent = 0 anim = Animation(show_percent=1, duration=app.animation_length) anim.start(self) if len(self.container.children) > 0: item_delay = app.animation_length / len( self.container.children) else: item_delay = 0 for i, w in enumerate(children): anim = ( Animation(duration=i * item_delay) + Animation(opacity=1, duration=app.animation_length)) w.opacity = 0 anim.start(w) else: self.opacity = 1 def dismiss(self, *args, **kwargs): app = App.get_running_app() if app.animations: anim = Animation(opacity=0, duration=app.animation_length) anim.start(self) anim.bind(on_complete=self.finish_dismiss) else: self.finish_dismiss() def finish_dismiss(self, *_): super(NormalDropDown, self).dismiss()
class Splitter(BoxLayout): '''See module documentation. :Events: `on_press`: Fired when the splitter is pressed. `on_release`: Fired when the splitter is released. .. versionchanged:: 1.6.0 Added `on_press` and `on_release` events. ''' border = ListProperty([4, 4, 4, 4]) '''Border used for the :class:`~kivy.graphics.vertex_instructions.BorderImage` graphics instruction. This must be a list of four values: (top, right, bottom, left). Read the BorderImage instructions for more information about how to use it. :attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to (4, 4, 4, 4). ''' strip_cls = ObjectProperty(SplitterStrip) '''Specifies the class of the resize Strip. :attr:`strip_cls` is an :class:`kivy.properties.ObjectProperty` and defaults to :class:`~kivy.uix.splitter.SplitterStrip`, which is of type :class:`~kivy.uix.button.Button`. .. versionchanged:: 1.8.0 If you set a string, the :class:`~kivy.factory.Factory` will be used to resolve the class. ''' sizable_from = OptionProperty('left', options=( 'left', 'right', 'top', 'bottom')) '''Specifies whether the widget is resizable. Options are:: `left`, `right`, `top` or `bottom` :attr:`sizable_from` is an :class:`~kivy.properties.OptionProperty` and defaults to `left`. ''' strip_size = NumericProperty('10pt') '''Specifies the size of resize strip :attr:`strp_size` is a :class:`~kivy.properties.NumericProperty` defaults to `10pt` ''' min_size = NumericProperty('100pt') '''Specifies the minimum size beyond which the widget is not resizable. :attr:`min_size` is a :class:`~kivy.properties.NumericProperty` and defaults to `100pt`. ''' max_size = NumericProperty('500pt') '''Specifies the maximum size beyond which the widget is not resizable. :attr:`max_size` is a :class:`~kivy.properties.NumericProperty` and defaults to `500pt`. ''' _parent_proportion = NumericProperty(0.) '''(internal) Specifies the distance that the slider has travelled across its parent, used to automatically maintain a sensible position if the parent is resized. :attr:`_parent_proportion` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. .. versionadded:: 1.9.0 ''' _bound_parent = ObjectProperty(None, allownone=True) '''(internal) References the widget whose size is currently being tracked by :attr:`_parent_proportion`. :attr:`_bound_parent` is a :class:`~kivy.properties.ObjectProperty` and defaults to None. .. versionadded:: 1.9.0 ''' keep_within_parent = BooleanProperty(False) '''If True, will limit the splitter to stay within its parent widget. :attr:`keep_within_parent` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. .. versionadded:: 1.9.0 ''' rescale_with_parent = BooleanProperty(False) '''If True, will automatically change size to take up the same proportion of the parent widget when it is resized, while staying within :attr:`min_size` and :attr:`max_size`. As long as these attributes can be satisfied, this stops the :class:`Splitter` from exceeding the parent size during rescaling. :attr:`rescale_with_parent` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. .. versionadded:: 1.9.0 ''' __events__ = ('on_press', 'on_release') def __init__(self, **kwargs): self._container = None self._strip = None super(Splitter, self).__init__(**kwargs) do_size = self._do_size fbind = self.fast_bind fbind('max_size', do_size) fbind('min_size', do_size) fbind('parent', self._rebind_parent) def on_sizable_from(self, instance, sizable_from): if not instance._container: return sup = super(Splitter, instance) _strp = instance._strip if _strp: # remove any previous binds _strp.unbind(on_touch_down=instance.strip_down) _strp.unbind(on_touch_move=instance.strip_move) _strp.unbind(on_touch_up=instance.strip_up) self.unbind(disabled=_strp.setter('disabled')) sup.remove_widget(instance._strip) else: cls = instance.strip_cls if isinstance(cls, string_types): cls = Factory.get(cls) instance._strip = _strp = cls() sz_frm = instance.sizable_from[0] if sz_frm in ('l', 'r'): _strp.size_hint = None, 1 _strp.width = instance.strip_size instance.orientation = 'horizontal' instance.unbind(strip_size=_strp.setter('width')) instance.bind(strip_size=_strp.setter('width')) else: _strp.size_hint = 1, None _strp.height = instance.strip_size instance.orientation = 'vertical' instance.unbind(strip_size=_strp.setter('height')) instance.bind(strip_size=_strp.setter('height')) index = 1 if sz_frm in ('r', 'b'): index = 0 sup.add_widget(_strp, index) _strp.bind(on_touch_down=instance.strip_down) _strp.bind(on_touch_move=instance.strip_move) _strp.bind(on_touch_up=instance.strip_up) _strp.disabled = self.disabled self.bind(disabled=_strp.setter('disabled')) def add_widget(self, widget, index=0): if self._container or not widget: return Exception('Splitter accepts only one Child') self._container = widget sz_frm = self.sizable_from[0] if sz_frm in ('l', 'r'): widget.size_hint_x = 1 else: widget.size_hint_y = 1 index = 0 if sz_frm in ('r', 'b'): index = 1 super(Splitter, self).add_widget(widget, index) self.on_sizable_from(self, self.sizable_from) def remove_widget(self, widget, *largs): super(Splitter, self).remove_widget(widget) if widget == self._container: self._container = None def clear_widgets(self): self.remove_widget(self._container) def strip_down(self, instance, touch): if not instance.collide_point(*touch.pos): return False touch.grab(self) self.dispatch('on_press') def on_press(self): pass def _rebind_parent(self, instance, new_parent): if self._bound_parent is not None: self._bound_parent.unbind(size=self.rescale_parent_proportion) if self.parent is not None: new_parent.bind(size=self.rescale_parent_proportion) self._bound_parent = new_parent self.rescale_parent_proportion() def rescale_parent_proportion(self, *args): if self.rescale_with_parent: parent_proportion = self._parent_proportion if self.sizable_from in ('top', 'bottom'): new_height = parent_proportion * self.parent.height self.height = max(self.min_size, min(new_height, self.max_size)) else: new_width = parent_proportion * self.parent.width self.width = max(self.min_size, min(new_width, self.max_size)) def _do_size(self, instance, value): if self.sizable_from[0] in ('l', 'r'): self.width = max(self.min_size, min(self.width, self.max_size)) else: self.height = max(self.min_size, min(self.height, self.max_size)) def strip_move(self, instance, touch): if touch.grab_current is not instance: return False max_size = self.max_size min_size = self.min_size sz_frm = self.sizable_from[0] if sz_frm in ('t', 'b'): diff_y = (touch.dy) if self.keep_within_parent: if sz_frm == 't' and (self.top + diff_y) > self.parent.top: diff_y = self.parent.top - self.top elif sz_frm == 'b' and (self.y + diff_y) < self.parent.y: diff_y = self.parent.y - self.y if sz_frm == 'b': diff_y *= -1 if self.size_hint_y: self.size_hint_y = None if self.height > 0: self.height += diff_y else: self.height = 1 height = self.height self.height = max(min_size, min(height, max_size)) self._parent_proportion = self.height / self.parent.height else: diff_x = (touch.dx) if self.keep_within_parent: if sz_frm == 'l' and (self.x + diff_x) < self.parent.x: diff_x = self.parent.x - self.x elif (sz_frm == 'r' and (self.right + diff_x) > self.parent.right): diff_x = self.parent.right - self.right if sz_frm == 'l': diff_x *= -1 if self.size_hint_x: self.size_hint_x = None if self.width > 0: self.width += diff_x else: self.width = 1 width = self.width self.width = max(min_size, min(width, max_size)) self._parent_proportion = self.width / self.parent.width def strip_up(self, instance, touch): if touch.grab_current is not instance: return if touch.is_double_tap: max_size = self.max_size min_size = self.min_size sz_frm = self.sizable_from[0] s = self.size if sz_frm in ('t', 'b'): if self.size_hint_y: self.size_hint_y = None if s[1] - min_size <= max_size - s[1]: self.height = max_size else: self.height = min_size else: if self.size_hint_x: self.size_hint_x = None if s[0] - min_size <= max_size - s[0]: self.width = max_size else: self.width = min_size touch.ungrab(instance) self.dispatch('on_release') def on_release(self): pass
class ElectrumWindow(App): electrum_config = ObjectProperty(None) language = StringProperty('en') # properties might be updated by the network num_blocks = NumericProperty(0) num_nodes = NumericProperty(0) server_host = StringProperty('') server_port = StringProperty('') num_chains = NumericProperty(0) blockchain_name = StringProperty('') fee_status = StringProperty('Fee') balance = StringProperty('') fiat_balance = StringProperty('') is_fiat = BooleanProperty(False) blockchain_checkpoint = NumericProperty(0) auto_connect = BooleanProperty(False) def on_auto_connect(self, instance, x): host, port, protocol, proxy, auto_connect = self.network.get_parameters() self.network.set_parameters(host, port, protocol, proxy, self.auto_connect) def toggle_auto_connect(self, x): self.auto_connect = not self.auto_connect def choose_server_dialog(self, popup): from .uix.dialogs.choice_dialog import ChoiceDialog protocol = 's' def cb2(host): from electrum_smart import constants pp = servers.get(host, constants.net.DEFAULT_PORTS) port = pp.get(protocol, '') popup.ids.host.text = host popup.ids.port.text = port servers = self.network.get_servers() ChoiceDialog(_('Choose a server'), sorted(servers), popup.ids.host.text, cb2).open() def choose_blockchain_dialog(self, dt): from .uix.dialogs.choice_dialog import ChoiceDialog chains = self.network.get_blockchains() def cb(name): for index, b in self.network.blockchains.items(): if name == self.network.get_blockchain_name(b): self.network.follow_chain(index) #self.block names = [self.network.blockchains[b].get_name() for b in chains] if len(names) >1: ChoiceDialog(_('Choose your chain'), names, '', cb).open() use_rbf = BooleanProperty(False) def on_use_rbf(self, instance, x): self.electrum_config.set_key('use_rbf', self.use_rbf, False) use_change = BooleanProperty(False) def on_use_change(self, instance, x): self.electrum_config.set_key('use_change', self.use_change, True) use_unconfirmed = BooleanProperty(False) def on_use_unconfirmed(self, instance, x): self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, True) def set_URI(self, uri): self.switch_to('send') self.send_screen.set_URI(uri) def on_new_intent(self, intent): if intent.getScheme() != 'bitcoin': return uri = intent.getDataString() self.set_URI(uri) def on_language(self, instance, language): Logger.info('language: {}'.format(language)) _.switch_lang(language) def update_history(self, *dt): if self.history_screen: self.history_screen.update() def on_quotes(self, d): Logger.info("on_quotes") self._trigger_update_history() def on_history(self, d): Logger.info("on_history") self._trigger_update_history() def _get_bu(self): return self.electrum_config.get('base_unit', 'mSMART') def _set_bu(self, value): assert value in base_units.keys() self.electrum_config.set_key('base_unit', value, True) self._trigger_update_status() self._trigger_update_history() base_unit = AliasProperty(_get_bu, _set_bu) status = StringProperty('') fiat_unit = StringProperty('') def on_fiat_unit(self, a, b): self._trigger_update_history() def decimal_point(self): return base_units[self.base_unit] def btc_to_fiat(self, amount_str): if not amount_str: return '' if not self.fx.is_enabled(): return '' rate = self.fx.exchange_rate() if rate.is_nan(): return '' fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8) return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.') def fiat_to_btc(self, fiat_amount): if not fiat_amount: return '' rate = self.fx.exchange_rate() if rate.is_nan(): return '' satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate)) return format_satoshis_plain(satoshis, self.decimal_point()) def get_amount(self, amount_str): a, u = amount_str.split() assert u == self.base_unit try: x = Decimal(a) except: return None p = pow(10, self.decimal_point()) return int(p * x) _orientation = OptionProperty('landscape', options=('landscape', 'portrait')) def _get_orientation(self): return self._orientation orientation = AliasProperty(_get_orientation, None, bind=('_orientation',)) '''Tries to ascertain the kind of device the app is running on. Cane be one of `tablet` or `phone`. :data:`orientation` is a read only `AliasProperty` Defaults to 'landscape' ''' _ui_mode = OptionProperty('phone', options=('tablet', 'phone')) def _get_ui_mode(self): return self._ui_mode ui_mode = AliasProperty(_get_ui_mode, None, bind=('_ui_mode',)) '''Defines tries to ascertain the kind of device the app is running on. Cane be one of `tablet` or `phone`. :data:`ui_mode` is a read only `AliasProperty` Defaults to 'phone' ''' def __init__(self, **kwargs): # initialize variables self._clipboard = Clipboard self.info_bubble = None self.nfcscanner = None self.tabs = None self.is_exit = False self.wallet = None self.pause_time = 0 App.__init__(self)#, **kwargs) title = _('Electrum App') self.electrum_config = config = kwargs.get('config', None) self.language = config.get('language', 'en') self.network = network = kwargs.get('network', None) if self.network: self.num_blocks = self.network.get_local_height() self.num_nodes = len(self.network.get_interfaces()) host, port, protocol, proxy_config, auto_connect = self.network.get_parameters() self.server_host = host self.server_port = port self.auto_connect = auto_connect self.proxy_config = proxy_config if proxy_config else {} self.plugins = kwargs.get('plugins', []) self.gui_object = kwargs.get('gui_object', None) self.daemon = self.gui_object.daemon self.fx = self.daemon.fx self.use_rbf = config.get('use_rbf', False) self.use_change = config.get('use_change', True) self.use_unconfirmed = not config.get('confirmed_only', False) # create triggers so as to minimize updation a max of 2 times a sec self._trigger_update_wallet = Clock.create_trigger(self.update_wallet, .5) self._trigger_update_status = Clock.create_trigger(self.update_status, .5) self._trigger_update_history = Clock.create_trigger(self.update_history, .5) self._trigger_update_interfaces = Clock.create_trigger(self.update_interfaces, .5) # cached dialogs self._settings_dialog = None self._password_dialog = None self.fee_status = self.electrum_config.get_fee_status() def wallet_name(self): return os.path.basename(self.wallet.storage.path) if self.wallet else ' ' def on_pr(self, pr): if pr.verify(self.wallet.contacts): key = self.wallet.invoices.add(pr) if self.invoices_screen: self.invoices_screen.update() status = self.wallet.invoices.get_status(key) if status == PR_PAID: self.show_error("invoice already paid") self.send_screen.do_clear() else: if pr.has_expired(): self.show_error(_('Payment request has expired')) else: self.switch_to('send') self.send_screen.set_request(pr) else: self.show_error("invoice error:" + pr.error) self.send_screen.do_clear() def on_qr(self, data): from electrum_smart.bitcoin import base_decode, is_address data = data.strip() if is_address(data): self.set_URI(data) return if data.startswith('smartcash:'): self.set_URI(data) return # try to decode transaction from electrum_smart.transaction import Transaction from electrum_smart.util import bh2u try: text = bh2u(base_decode(data, None, base=43)) tx = Transaction(text) tx.deserialize() except: tx = None if tx: self.tx_dialog(tx) return # show error self.show_error("Unable to decode QR data") def update_tab(self, name): s = getattr(self, name + '_screen', None) if s: s.update() @profiler def update_tabs(self): for tab in ['invoices', 'send', 'history', 'receive', 'address']: self.update_tab(tab) def switch_to(self, name): s = getattr(self, name + '_screen', None) if s is None: s = self.tabs.ids[name + '_screen'] s.load_screen() panel = self.tabs.ids.panel tab = self.tabs.ids[name + '_tab'] panel.switch_to(tab) def show_request(self, addr): self.switch_to('receive') self.receive_screen.screen.address = addr def show_pr_details(self, req, status, is_invoice): from electrum_smart.util import format_time requestor = req.get('requestor') exp = req.get('exp') memo = req.get('memo') amount = req.get('amount') fund = req.get('fund') popup = Builder.load_file('gui/kivy/uix/ui_screens/invoice.kv') popup.is_invoice = is_invoice popup.amount = amount popup.requestor = requestor if is_invoice else req.get('address') popup.exp = format_time(exp) if exp else '' popup.description = memo if memo else '' popup.signature = req.get('signature', '') popup.status = status popup.fund = fund if fund else 0 txid = req.get('txid') popup.tx_hash = txid or '' popup.on_open = lambda: popup.ids.output_list.update(req.get('outputs', [])) popup.export = self.export_private_keys popup.open() def show_addr_details(self, req, status): from electrum_smart.util import format_time fund = req.get('fund') isaddr = 'y' popup = Builder.load_file('gui/kivy/uix/ui_screens/invoice.kv') popup.isaddr = isaddr popup.is_invoice = False popup.status = status popup.requestor = req.get('address') popup.fund = fund if fund else 0 popup.export = self.export_private_keys popup.open() def qr_dialog(self, title, data, show_text=False): from .uix.dialogs.qr_dialog import QRDialog popup = QRDialog(title, data, show_text) popup.open() def scan_qr(self, on_complete): if platform != 'android': return from jnius import autoclass, cast from android import activity PythonActivity = autoclass('org.kivy.android.PythonActivity') SimpleScannerActivity = autoclass("org.electrum.qr.SimpleScannerActivity") Intent = autoclass('android.content.Intent') intent = Intent(PythonActivity.mActivity, SimpleScannerActivity) def on_qr_result(requestCode, resultCode, intent): if resultCode == -1: # RESULT_OK: # this doesn't work due to some bug in jnius: # contents = intent.getStringExtra("text") String = autoclass("java.lang.String") contents = intent.getStringExtra(String("text")) on_complete(contents) activity.bind(on_activity_result=on_qr_result) PythonActivity.mActivity.startActivityForResult(intent, 0) def do_share(self, data, title): if platform != 'android': return from jnius import autoclass, cast JS = autoclass('java.lang.String') Intent = autoclass('android.content.Intent') sendIntent = Intent() sendIntent.setAction(Intent.ACTION_SEND) sendIntent.setType("text/plain") sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data)) PythonActivity = autoclass('org.kivy.android.PythonActivity') currentActivity = cast('android.app.Activity', PythonActivity.mActivity) it = Intent.createChooser(sendIntent, cast('java.lang.CharSequence', JS(title))) currentActivity.startActivity(it) def build(self): return Builder.load_file('gui/kivy/main.kv') def _pause(self): if platform == 'android': # move activity to back from jnius import autoclass python_act = autoclass('org.kivy.android.PythonActivity') mActivity = python_act.mActivity mActivity.moveTaskToBack(True) def on_start(self): ''' This is the start point of the kivy ui ''' import time Logger.info('Time to on_start: {} <<<<<<<<'.format(time.clock())) win = Window win.bind(size=self.on_size, on_keyboard=self.on_keyboard) win.bind(on_key_down=self.on_key_down) #win.softinput_mode = 'below_target' self.on_size(win, win.size) self.init_ui() # init plugins run_hook('init_kivy', self) # fiat currency self.fiat_unit = self.fx.ccy if self.fx.is_enabled() else '' # default tab self.switch_to('history') # bind intent for bitcoin: URI scheme if platform == 'android': from android import activity from jnius import autoclass PythonActivity = autoclass('org.kivy.android.PythonActivity') mactivity = PythonActivity.mActivity self.on_new_intent(mactivity.getIntent()) activity.bind(on_new_intent=self.on_new_intent) # connect callbacks if self.network: interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces'] self.network.register_callback(self.on_network_event, interests) self.network.register_callback(self.on_fee, ['fee']) self.network.register_callback(self.on_quotes, ['on_quotes']) self.network.register_callback(self.on_history, ['on_history']) # load wallet self.load_wallet_by_name(self.electrum_config.get_wallet_path()) # URI passed in config uri = self.electrum_config.get('url') if uri: self.set_URI(uri) def get_wallet_path(self): if self.wallet: return self.wallet.storage.path else: return '' def on_wizard_complete(self, instance, wallet): if wallet: wallet.start_threads(self.daemon.network) self.daemon.add_wallet(wallet) self.load_wallet(wallet) def load_wallet_by_name(self, path): if not path: return if self.wallet and self.wallet.storage.path == path: return wallet = self.daemon.load_wallet(path, None) if wallet: if wallet.has_password(): self.password_dialog(wallet, _('Enter PIN code'), lambda x: self.load_wallet(wallet), self.stop) else: self.load_wallet(wallet) else: Logger.debug('Electrum: Wallet not found. Launching install wizard') storage = WalletStorage(path) wizard = Factory.InstallWizard(self.electrum_config, storage) wizard.bind(on_wizard_complete=self.on_wizard_complete) action = wizard.storage.get_action() wizard.run(action) def on_stop(self): Logger.info('on_stop') self.stop_wallet() def stop_wallet(self): if self.wallet: self.daemon.stop_wallet(self.wallet.storage.path) self.wallet = None def on_key_down(self, instance, key, keycode, codepoint, modifiers): if 'ctrl' in modifiers: # q=24 w=25 if keycode in (24, 25): self.stop() elif keycode == 27: # r=27 # force update wallet self.update_wallet() elif keycode == 112: # pageup #TODO move to next tab pass elif keycode == 117: # pagedown #TODO move to prev tab pass #TODO: alt+tab_number to activate the particular tab def on_keyboard(self, instance, key, keycode, codepoint, modifiers): if key == 27 and self.is_exit is False: self.is_exit = True self.show_info(_('Press again to exit')) return True # override settings button if key in (319, 282): #f1/settings button on android #self.gui.main_gui.toggle_settings(self) return True def settings_dialog(self): from .uix.dialogs.settings import SettingsDialog if self._settings_dialog is None: self._settings_dialog = SettingsDialog(self) self._settings_dialog.update() self._settings_dialog.open() def popup_dialog(self, name): if name == 'settings': self.settings_dialog() elif name == 'wallets': from .uix.dialogs.wallets import WalletDialog d = WalletDialog() d.open() else: popup = Builder.load_file('gui/kivy/uix/ui_screens/'+name+'.kv') popup.open() @profiler def init_ui(self): ''' Initialize The Ux part of electrum. This function performs the basic tasks of setting up the ui. ''' #from weakref import ref self.funds_error = False # setup UX self.screens = {} #setup lazy imports for mainscreen Factory.register('AnimatedPopup', module='electrum_smart_gui.kivy.uix.dialogs') Factory.register('QRCodeWidget', module='electrum_smart_gui.kivy.uix.qrcodewidget') # preload widgets. Remove this if you want to load the widgets on demand #Cache.append('electrum_widgets', 'AnimatedPopup', Factory.AnimatedPopup()) #Cache.append('electrum_widgets', 'QRCodeWidget', Factory.QRCodeWidget()) # load and focus the ui self.root.manager = self.root.ids['manager'] self.history_screen = None self.contacts_screen = None self.send_screen = None self.invoices_screen = None self.receive_screen = None self.requests_screen = None self.address_screen = None self.icon = "icons/electrum-smart.png" self.tabs = self.root.ids['tabs'] def update_interfaces(self, dt): self.num_nodes = len(self.network.get_interfaces()) self.num_chains = len(self.network.get_blockchains()) chain = self.network.blockchain() self.blockchain_checkpoint = chain.get_checkpoint() self.blockchain_name = chain.get_name() if self.network.interface: self.server_host = self.network.interface.host def on_network_event(self, event, *args): Logger.info('network event: '+ event) if event == 'interfaces': self._trigger_update_interfaces() elif event == 'updated': self._trigger_update_wallet() self._trigger_update_status() elif event == 'status': self._trigger_update_status() elif event == 'new_transaction': self._trigger_update_wallet() elif event == 'verified': self._trigger_update_wallet() @profiler def load_wallet(self, wallet): if self.wallet: self.stop_wallet() self.wallet = wallet self.update_wallet() # Once GUI has been initialized check if we want to announce something # since the callback has been called before the GUI was initialized if self.receive_screen: self.receive_screen.clear() self.update_tabs() run_hook('load_wallet', wallet, self) def update_status(self, *dt): self.num_blocks = self.network.get_local_height() if not self.wallet: self.status = _("No Wallet") return if self.network is None or not self.network.is_running(): status = _("Offline") elif self.network.is_connected(): server_height = self.network.get_server_height() server_lag = self.network.get_local_height() - server_height if not self.wallet.up_to_date or server_height == 0: status = _("Synchronizing...") elif server_lag > 1: status = _("Server lagging") else: status = '' else: status = _("Disconnected") self.status = self.wallet.basename() + (' [size=15dp](%s)[/size]'%status if status else '') # balance c, u, x = self.wallet.get_balance() text = self.format_amount(c+x+u) self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy def get_max_amount(self): inputs = self.wallet.get_spendable_coins(None, self.electrum_config) addr = str(self.send_screen.screen.address) or self.wallet.dummy_address() outputs = [(TYPE_ADDRESS, addr, '!')] tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config) amount = tx.output_value() return format_satoshis_plain(amount, self.decimal_point()) def format_amount(self, x, is_diff=False, whitespaces=False): return format_satoshis(x, is_diff, 0, self.decimal_point(), whitespaces) def format_amount_and_units(self, x): return format_satoshis_plain(x, self.decimal_point()) + ' ' + self.base_unit #@profiler def update_wallet(self, *dt): self._trigger_update_status() if self.wallet and (self.wallet.up_to_date or not self.network or not self.network.is_connected()): self.update_tabs() def notify(self, message): try: global notification, os if not notification: from plyer import notification icon = (os.path.dirname(os.path.realpath(__file__)) + '/../../' + self.icon) notification.notify('Electrum', message, app_icon=icon, app_name='Electrum') except ImportError: Logger.Error('Notification: needs plyer; `sudo pip install plyer`') def on_pause(self): self.pause_time = time.time() # pause nfc if self.nfcscanner: self.nfcscanner.nfc_disable() return True def on_resume(self): now = time.time() if self.wallet.has_password and now - self.pause_time > 60: self.password_dialog(self.wallet, _('Enter PIN'), None, self.stop) if self.nfcscanner: self.nfcscanner.nfc_enable() def on_size(self, instance, value): width, height = value self._orientation = 'landscape' if width > height else 'portrait' self._ui_mode = 'tablet' if min(width, height) > inch(3.51) else 'phone' def on_ref_label(self, label, touch): if label.touched: label.touched = False self.qr_dialog(label.name, label.data, True) else: label.touched = True self._clipboard.copy(label.data) Clock.schedule_once(lambda dt: self.show_info(_('Text copied to clipboard.\nTap again to display it as QR code.'))) def set_send(self, address, amount, label, message): self.send_payment(address, amount=amount, label=label, message=message) def show_error(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, icon='atlas://gui/kivy/theming/light/error', duration=0, modal=False): ''' Show a error Message Bubble. ''' self.show_info_bubble( text=error, icon=icon, width=width, pos=pos or Window.center, arrow_pos=arrow_pos, exit=exit, duration=duration, modal=modal) def show_info(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, duration=0, modal=False): ''' Show a Info Message Bubble. ''' self.show_error(error, icon='atlas://gui/kivy/theming/light/important', duration=duration, modal=modal, exit=exit, pos=pos, arrow_pos=arrow_pos) def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0, arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False): '''Method to show a Information Bubble .. parameters:: text: Message to be displayed pos: position for the bubble duration: duration the bubble remains on screen. 0 = click to hide width: width of the Bubble arrow_pos: arrow position for the bubble ''' info_bubble = self.info_bubble if not info_bubble: info_bubble = self.info_bubble = Factory.InfoBubble() win = Window if info_bubble.parent: win.remove_widget(info_bubble if not info_bubble.modal else info_bubble._modal_view) if not arrow_pos: info_bubble.show_arrow = False else: info_bubble.show_arrow = True info_bubble.arrow_pos = arrow_pos img = info_bubble.ids.img if text == 'texture': # icon holds a texture not a source image # display the texture in full screen text = '' img.texture = icon info_bubble.fs = True info_bubble.show_arrow = False img.allow_stretch = True info_bubble.dim_background = True info_bubble.background_image = 'atlas://gui/kivy/theming/light/card' else: info_bubble.fs = False info_bubble.icon = icon #if img.texture and img._coreimage: # img.reload() img.allow_stretch = False info_bubble.dim_background = False info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble' info_bubble.message = text if not pos: pos = (win.center[0], win.center[1] - (info_bubble.height/2)) info_bubble.show(pos, duration, width, modal=modal, exit=exit) def tx_dialog(self, tx): from .uix.dialogs.tx_dialog import TxDialog d = TxDialog(self, tx) d.open() def sign_tx(self, *args): threading.Thread(target=self._sign_tx, args=args).start() def _sign_tx(self, tx, password, on_success, on_failure): try: self.wallet.sign_transaction(tx, password) except InvalidPassword: Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN"))) return Clock.schedule_once(lambda dt: on_success(tx)) def _broadcast_thread(self, tx, on_complete): ok, txid = self.network.broadcast(tx) Clock.schedule_once(lambda dt: on_complete(ok, txid)) def broadcast(self, tx, pr=None): def on_complete(ok, msg): if ok: self.show_info(_('Payment sent.')) if self.send_screen: self.send_screen.do_clear() if pr: self.wallet.invoices.set_paid(pr, tx.txid()) self.wallet.invoices.save() self.update_tab('invoices') else: self.show_error(msg) if self.network and self.network.is_connected(): self.show_info(_('Sending')) threading.Thread(target=self._broadcast_thread, args=(tx, on_complete)).start() else: self.show_info(_('Cannot broadcast transaction') + ':\n' + _('Not connected')) def description_dialog(self, screen): from .uix.dialogs.label_dialog import LabelDialog text = screen.message def callback(text): screen.message = text d = LabelDialog(_('Enter description'), text, callback) d.open() def amount_dialog(self, screen, show_max): from .uix.dialogs.amount_dialog import AmountDialog amount = screen.amount if amount: amount, u = str(amount).split() assert u == self.base_unit def cb(amount): screen.amount = amount popup = AmountDialog(show_max, amount, cb) popup.open() def invoices_dialog(self, screen): from .uix.dialogs.invoices import InvoicesDialog if len(self.wallet.invoices.sorted_list()) == 0: self.show_info(' '.join([ _('No saved invoices.'), _('Signed invoices are saved automatically when you scan them.'), _('You may also save unsigned requests or contact addresses using the save button.') ])) return popup = InvoicesDialog(self, screen, None) popup.update() popup.open() def requests_dialog(self, screen): from .uix.dialogs.requests import RequestsDialog if len(self.wallet.get_sorted_requests(self.electrum_config)) == 0: self.show_info(_('No saved requests.')) return popup = RequestsDialog(self, screen, None) popup.update() popup.open() def addresses_dialog(self, screen): from .uix.dialogs.addresses import AddressesDialog popup = AddressesDialog(self, screen, None) popup.update() popup.open() def fee_dialog(self, label, dt): from .uix.dialogs.fee_dialog import FeeDialog def cb(): self.fee_status = self.electrum_config.get_fee_status() fee_dialog = FeeDialog(self, self.electrum_config, cb) fee_dialog.open() def on_fee(self, event, *arg): self.fee_status = self.electrum_config.get_fee_status() def protected(self, msg, f, args): if self.wallet.has_password(): on_success = lambda pw: f(*(args + (pw,))) self.password_dialog(self.wallet, msg, on_success, lambda: None) else: f(*(args + (None,))) def delete_wallet(self): from .uix.dialogs.question import Question basename = os.path.basename(self.wallet.storage.path) d = Question(_('Delete wallet?') + '\n' + basename, self._delete_wallet) d.open() def _delete_wallet(self, b): if b: basename = self.wallet.basename() self.protected(_("Enter your PIN code to confirm deletion of {}").format(basename), self.__delete_wallet, ()) def __delete_wallet(self, pw): wallet_path = self.get_wallet_path() dirname = os.path.dirname(wallet_path) basename = os.path.basename(wallet_path) if self.wallet.has_password(): try: self.wallet.check_password(pw) except: self.show_error("Invalid PIN") return self.stop_wallet() os.unlink(wallet_path) self.show_error("Wallet removed:" + basename) d = os.listdir(dirname) name = 'default_wallet' new_path = os.path.join(dirname, name) self.load_wallet_by_name(new_path) def show_seed(self, label): self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,)) def _show_seed(self, label, password): if self.wallet.has_password() and password is None: return keystore = self.wallet.keystore try: seed = keystore.get_seed(password) passphrase = keystore.get_passphrase(password) except: self.show_error("Invalid PIN") return label.text = _('Seed') + ':\n' + seed if passphrase: label.text += '\n\n' + _('Passphrase') + ': ' + passphrase def password_dialog(self, wallet, msg, on_success, on_failure): from .uix.dialogs.password_dialog import PasswordDialog if self._password_dialog is None: self._password_dialog = PasswordDialog() self._password_dialog.init(self, wallet, msg, on_success, on_failure) self._password_dialog.open() def change_password(self, cb): from .uix.dialogs.password_dialog import PasswordDialog if self._password_dialog is None: self._password_dialog = PasswordDialog() message = _("Changing PIN code.") + '\n' + _("Enter your current PIN:") def on_success(old_password, new_password): self.wallet.update_password(old_password, new_password) self.show_info(_("Your PIN code was updated")) on_failure = lambda: self.show_error(_("PIN codes do not match")) self._password_dialog.init(self, self.wallet, message, on_success, on_failure, is_change=1) self._password_dialog.open() def export_private_keys(self, pk_label, addr): if self.wallet.is_watching_only(): self.show_info(_('This is a watching-only wallet. It does not contain private keys.')) return def show_private_key(addr, pk_label, password): if self.wallet.has_password() and password is None: return if not self.wallet.can_export(): return try: key = str(self.wallet.export_private_key(addr, password)[0]) pk_label.data = key except InvalidPassword: self.show_error("Invalid PIN") return self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label))
class Image(Widget): '''Image class, see module documentation for more information. ''' source = StringProperty(None) '''Filename / source of your image. :attr:`source` is a :class:`~kivy.properties.StringProperty` and defaults to None. ''' texture = ObjectProperty(None, allownone=True) '''Texture object of the image. The texture represents the original, loaded image texture. It is stretched and positioned during rendering according to the :attr:`allow_stretch` and :attr:`keep_ratio` properties. Depending of the texture creation, the value will be a :class:`~kivy.graphics.texture.Texture` or a :class:`~kivy.graphics.texture.TextureRegion` object. :attr:`texture` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' texture_size = ListProperty([0, 0]) '''Texture size of the image. This represents the original, loaded image texture size. .. warning:: The texture size is set after the texture property. So if you listen to the change on :attr:`texture`, the property texture_size will not be up-to-date. Use self.texture.size instead. ''' def get_image_ratio(self): if self.texture: return self.texture.width / float(self.texture.height) return 1. mipmap = BooleanProperty(False) '''Indicate if you want OpenGL mipmapping to be applied to the texture. Read :ref:`mipmap` for more information. .. versionadded:: 1.0.7 :attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' image_ratio = AliasProperty(get_image_ratio, bind=('texture', ), cache=True) '''Ratio of the image (width / float(height). :attr:`image_ratio` is an :class:`~kivy.properties.AliasProperty` and is read-only. ''' color = ColorProperty([1, 1, 1, 1]) '''Image color, in the format (r, g, b, a). This attribute can be used to 'tint' an image. Be careful: if the source image is not gray/white, the color will not really work as expected. .. versionadded:: 1.0.6 :attr:`color` is a :class:`~kivy.properties.ColorProperty` and defaults to [1, 1, 1, 1]. .. versionchanged:: 2.0.0 Changed from :class:`~kivy.properties.ListProperty` to :class:`~kivy.properties.ColorProperty`. ''' allow_stretch = BooleanProperty(False) '''If True, the normalized image size will be maximized to fit in the image box. Otherwise, if the box is too tall, the image will not be stretched more than 1:1 pixels. .. versionadded:: 1.0.7 :attr:`allow_stretch` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' keep_ratio = BooleanProperty(True) '''If False along with allow_stretch being True, the normalized image size will be maximized to fit in the image box and ignores the aspect ratio of the image. Otherwise, if the box is too tall, the image will not be stretched more than 1:1 pixels. .. versionadded:: 1.0.8 :attr:`keep_ratio` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' keep_data = BooleanProperty(False) '''If True, the underlaying _coreimage will store the raw image data. This is useful when performing pixel based collision detection. .. versionadded:: 1.3.0 :attr:`keep_data` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' anim_delay = NumericProperty(.25) '''Delay the animation if the image is sequenced (like an animated gif). If anim_delay is set to -1, the animation will be stopped. .. versionadded:: 1.0.8 :attr:`anim_delay` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.25 (4 FPS). ''' anim_loop = NumericProperty(0) '''Number of loops to play then stop animating. 0 means keep animating. .. versionadded:: 1.9.0 :attr:`anim_loop` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' nocache = BooleanProperty(False) '''If this property is set True, the image will not be added to the internal cache. The cache will simply ignore any calls trying to append the core image. .. versionadded:: 1.6.0 :attr:`nocache` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' def get_norm_image_size(self): if not self.texture: return list(self.size) ratio = self.image_ratio w, h = self.size tw, th = self.texture.size # ensure that the width is always maximized to the containter width if self.allow_stretch: if not self.keep_ratio: return [w, h] iw = w else: iw = min(w, tw) # calculate the appropriate height ih = iw / ratio # if the height is too higher, take the height of the container # and calculate appropriate width. no need to test further. :) if ih > h: if self.allow_stretch: ih = h else: ih = min(h, th) iw = ih * ratio return [iw, ih] norm_image_size = AliasProperty(get_norm_image_size, bind=('texture', 'size', 'allow_stretch', 'image_ratio', 'keep_ratio'), cache=True) '''Normalized image size within the widget box. This size will always fit the widget size and will preserve the image ratio. :attr:`norm_image_size` is an :class:`~kivy.properties.AliasProperty` and is read-only. ''' def __init__(self, **kwargs): self._coreimage = None self._loops = 0 update = self.texture_update fbind = self.fbind fbind('source', update) fbind('mipmap', update) super().__init__(**kwargs) def texture_update(self, *largs): if not self.source: self._clear_core_image() return source = resource_find(self.source) if not source: Logger.error('Image: Not found <%s>' % self.source) self._clear_core_image() return if self._coreimage: self._coreimage.unbind(on_texture=self._on_tex_change) try: self._coreimage = image = CoreImage(source, mipmap=self.mipmap, anim_delay=self.anim_delay, keep_data=self.keep_data, nocache=self.nocache) except Exception: Logger.error('Image: Error loading <%s>' % self.source) self._clear_core_image() image = self._coreimage if image: image.bind(on_texture=self._on_tex_change) self.texture = image.texture def on_anim_delay(self, instance, value): if self._coreimage is None: return self._coreimage.anim_delay = value if value < 0: self._coreimage.anim_reset(False) def on_texture(self, instance, value): self.texture_size = value.size if value else [0, 0] def _clear_core_image(self): if self._coreimage: self._coreimage.unbind(on_texture=self._on_tex_change) self.texture = None self._coreimage = None self._loops = 0 def _on_tex_change(self, *largs): # update texture from core image self.texture = self._coreimage.texture ci = self._coreimage if self.anim_loop and ci._anim_index == len(ci._image.textures) - 1: self._loops += 1 if self.anim_loop == self._loops: ci.anim_reset(False) self._loops = 0 def reload(self): '''Reload image from disk. This facilitates re-loading of images from disk in case the image content changes. .. versionadded:: 1.3.0 Usage:: im = Image(source = '1.jpg') # -- do something -- im.reload() # image will be re-loaded from disk ''' self.remove_from_cache() old_source = self.source self.source = '' self.source = old_source def remove_from_cache(self): '''Remove image from cache. .. versionadded:: 2.0.0 ''' if self._coreimage: self._coreimage.remove_from_cache() def on_nocache(self, *args): if self.nocache: self.remove_from_cache() if self._coreimage: self._coreimage._nocache = True
class BaseSnackbar(MDCard): """ :Events: :attr:`on_open` Called when a dialog is opened. :attr:`on_dismiss` When the front layer rises. Abstract base class for all Snackbars. This class handles sizing, positioning, shape and events for Snackbars All Snackbars will be made off of this `BaseSnackbar`. `BaseSnackbar` will always try to fill the remainder of the screen with your Snackbar. To make your Snackbar dynamic and symetric with snackbar_x. Set size_hint_x like below: .. code-block:: python size_hint_z = ( Window.width - (snackbar_x * 2) ) / Window.width """ duration = NumericProperty(3) """ The amount of time that the snackbar will stay on screen for. :attr:`duration` is a :class:`~kivy.properties.NumericProperty` and defaults to `3`. """ auto_dismiss = BooleanProperty(True) """ Whether to use automatic closing of the snackbar or not. :attr:`auto_dismiss` is a :class:`~kivy.properties.BooleanProperty` and defaults to `'True'`. """ bg_color = ListProperty() """ Snackbar background. :attr:`bg_color` is a :class:`~kivy.properties.ListProperty` and defaults to `'[]'`. """ buttons = ListProperty() """ Snackbar buttons. :attr:`buttons` is a :class:`~kivy.properties.ListProperty` and defaults to `'[]'` """ radius = ListProperty([5, 5, 5, 5]) """ Snackbar radius. :attr:`radius` is a :class:`~kivy.properties.ListProperty` and defaults to `'[5, 5, 5, 5]'` """ snackbar_animation_dir = OptionProperty( "Bottom", options=[ "Top", "Bottom", "Left", "Right", ], ) """ Snackbar animation direction. Available options are: `"Top"`, `"Bottom"`, `"Left"`, `"Right"` :attr:`snackbar_animation_dir` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Bottom'`. """ snackbar_x = NumericProperty("0dp") """ The snackbar x position in the screen :attr:`snackbar_x` is a :class:`~kivy.properties.NumericProperty` and defaults to `0dp`. """ snackbar_y = NumericProperty("0dp") """ The snackbar x position in the screen :attr:`snackbar_y` is a :class:`~kivy.properties.NumericProperty` and defaults to `0dp`. """ _interval = 0 def __init__(self, **kwargs): super().__init__(**kwargs) self.register_event_type("on_open") self.register_event_type("on_dismiss") def dismiss(self, *args): """Dismiss the snackbar.""" def dismiss(interval): if self.snackbar_animation_dir == "Top": anim = Animation(y=(Window.height + self.height), d=0.2) elif self.snackbar_animation_dir == "Left": anim = Animation(x=-self.width, d=0.2) elif self.snackbar_animation_dir == "Right": anim = Animation(x=Window.width, d=0.2) else: anim = Animation(y=-self.height, d=0.2) anim.bind( on_complete=lambda *args: Window.parent.remove_widget(self) ) anim.start(self) Clock.schedule_once(dismiss, 0.5) self.dispatch("on_dismiss") def open(self): """Show the snackbar.""" def wait_interval(interval): self._interval += interval if self._interval > self.duration: self.dismiss() Clock.unschedule(wait_interval) self._interval = 0 for c in Window.parent.children: if isinstance(c, BaseSnackbar): return if self.snackbar_y > (Window.height - self.height): self.snackbar_y = Window.height - self.height self._calc_radius() if self.size_hint_x == 1: self.size_hint_x = (Window.width - self.snackbar_x) / Window.width if ( self.snackbar_animation_dir == "Top" or self.snackbar_animation_dir == "Bottom" ): self.x = self.snackbar_x if self.snackbar_animation_dir == "Top": self.y = Window.height + self.height else: self.y = -self.height Window.parent.add_widget(self) if self.snackbar_animation_dir == "Top": anim = Animation( y=self.snackbar_y if self.snackbar_y != 0 else Window.height - self.height, d=0.2, ) else: anim = Animation( y=self.snackbar_y if self.snackbar_y != 0 else 0, d=0.2 ) elif ( self.snackbar_animation_dir == "Left" or self.snackbar_animation_dir == "Right" ): self.y = self.snackbar_y if self.snackbar_animation_dir == "Left": self.x = -Window.width else: self.x = Window.width Window.parent.add_widget(self) anim = Animation( x=self.snackbar_x if self.snackbar_x != 0 else 0, d=0.2 ) if self.auto_dismiss: anim.bind( on_complete=lambda *args: Clock.schedule_interval( wait_interval, 0 ) ) anim.start(self) self.dispatch("on_open") def on_open(self, *args): """Called when a dialog is opened.""" def on_dismiss(self, *args): """Called when the dialog is closed.""" def on_buttons(self, instance, value): def on_buttons(interval): for button in value: if issubclass(button.__class__, (BaseButton,)): self.add_widget(button) else: raise ValueError( f"The {button} object must be inherited from the base class <BaseButton>" ) Clock.schedule_once(on_buttons) def _calc_radius(self): if ( self.snackbar_animation_dir == "Top" or self.snackbar_animation_dir == "Bottom" ): if self.snackbar_y == 0 and self.snackbar_x == 0: if self.size_hint_x == 1: self.radius = [0, 0, 0, 0] else: if self.snackbar_animation_dir == "Top": self.radius = [0, 0, self.radius[2], 0] else: self.radius = [0, self.radius[1], 0, 0] elif self.snackbar_y != 0 and self.snackbar_x == 0: if self.size_hint_x == 1: self.radius = [0, 0, 0, 0] else: if self.snackbar_y >= Window.height - self.height: self.radius = [0, 0, self.radius[2], 0] else: self.radius = [0, self.radius[1], self.radius[2], 0] elif self.snackbar_y == 0 and self.snackbar_x != 0: if self.size_hint_x == 1: if self.snackbar_animation_dir == "Top": self.radius = [0, 0, 0, self.radius[3]] else: self.radius = [self.radius[0], 0, 0, 0] else: if self.snackbar_animation_dir == "Top": self.radius = [0, 0, self.radius[2], self.radius[3]] else: self.radius = [self.radius[0], self.radius[1], 0, 0] else: # self.snackbar_y != 0 and self.snackbar_x != 0 if self.size_hint_x == 1: self.radius = [self.radius[0], 0, 0, self.radius[3]] elif self.snackbar_y >= Window.height - self.height: self.radius = [0, 0, self.radius[2], self.radius[3]] elif ( self.snackbar_animation_dir == "Left" or self.snackbar_animation_dir == "Right" ): if self.snackbar_y == 0 and self.snackbar_x == 0: if self.size_hint_x == 1: self.radius = [0, 0, 0, 0] else: self.radius = [0, self.radius[1], 0, 0] elif self.snackbar_y != 0 and self.snackbar_x == 0: if self.size_hint_x == 1: self.radius = [0, 0, 0, 0] else: self.radius = [0, self.radius[1], self.radius[2], 0] elif self.snackbar_y == 0 and self.snackbar_x != 0: if self.size_hint_x == 1: self.radius = [self.radius[0], 0, 0, 0] else: self.radius = [self.radius[0], self.radius[1], 0, 0] else: # self.snackbar_y != 0 and self.snackbar_x != 0 if self.size_hint_x == 1: if self.snackbar_y >= Window.height - self.height: self.radius = [0, 0, 0, self.radius[3]] else: self.radius = [self.radius[0], 0, 0, self.radius[3]] elif self.snackbar_y >= Window.height - self.height: self.radius = [0, 0, self.radius[2], self.radius[3]]