A few bug fixes and a whole lot of documentation Mantis: 2691
git-svn-id: file:///srv/svn/scanner/trunk@16 a0501263-5b7a-4423-a8ba-1edf086583e7
This commit is contained in:
parent
e620c4fc30
commit
57215e1191
3 changed files with 383 additions and 49 deletions
|
@ -1,6 +1,22 @@
|
|||
<?php
|
||||
/**
|
||||
* Example ncurses application
|
||||
*
|
||||
* @package Nc
|
||||
* @subpackage Example
|
||||
*/
|
||||
|
||||
/**
|
||||
* Include ncurses library
|
||||
*/
|
||||
require_once( 'ncurses.php' );
|
||||
|
||||
/**
|
||||
* Example Nc Application
|
||||
*
|
||||
* @package Nc
|
||||
* @subpackage Example
|
||||
*/
|
||||
class MyApp extends NcApp {
|
||||
function run() {
|
||||
$nc_main = new NcWindow( $this, 0, 0, 0, 0 );
|
||||
|
|
|
@ -1,8 +1,34 @@
|
|||
<?php
|
||||
class NcApp {
|
||||
/**
|
||||
* PHP ncurses library
|
||||
*
|
||||
* Provides a clean, object-oriented interface to the PHP ncurses functions to ease
|
||||
* the creation of elaborate, functional CLI applications.
|
||||
*
|
||||
* @author Correl J. Roush <correl@gmail.com>
|
||||
* @copyright Copyright (c) 2008, Correl J. Roush
|
||||
* @version 0.1
|
||||
*
|
||||
* @package Nc
|
||||
*/
|
||||
|
||||
/**
|
||||
* PHP ncurses application
|
||||
*
|
||||
* @package Nc
|
||||
*/
|
||||
abstract class NcApp {
|
||||
var $screen;
|
||||
|
||||
function __construct( $echo = false, $cursor = false ) {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Initializes the ncurses environment, as well as the fullscreen window {@link $screen}
|
||||
*
|
||||
* @param boolean $echo Echo keyboard input to the screen
|
||||
* @param boolean $cursor Display the cursor on the screen
|
||||
*/
|
||||
public function __construct( $echo = false, $cursor = false ) {
|
||||
ncurses_init();
|
||||
$echo ? ncurses_echo() : ncurses_noecho();
|
||||
ncurses_curs_set( (bool)$cursor );
|
||||
|
@ -10,31 +36,91 @@ class NcApp {
|
|||
ncurses_refresh();
|
||||
$this->run();
|
||||
}
|
||||
function __destruct() {
|
||||
/**
|
||||
* Destructor
|
||||
*
|
||||
* Clears the screen and ends the ncurses session
|
||||
*/
|
||||
public function __destruct() {
|
||||
ncurses_clear();
|
||||
ncurses_refresh();
|
||||
ncurses_end();
|
||||
}
|
||||
function run() {
|
||||
}
|
||||
/**
|
||||
* This is a placeholder function for application code
|
||||
*/
|
||||
abstract function run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ncurses window
|
||||
*
|
||||
* Provides a basic window object. A border can be optionally enabled if the window
|
||||
* is large enough to accomodate it, and is enabled by default. If a title is set,
|
||||
* it will be visible at the top of the border if one exists.
|
||||
*
|
||||
* If a parent window is provided, the window will be registered as a child of the
|
||||
* parent, and will be relatively positioned to the parent.
|
||||
*
|
||||
* @package Nc
|
||||
*/
|
||||
class NcWindow {
|
||||
/**
|
||||
* @var resource nc_window resource for the border window, if one exists
|
||||
*/
|
||||
var $frame;
|
||||
/**
|
||||
* @var resource nc_window resource for the container window
|
||||
*/
|
||||
var $window;
|
||||
/**
|
||||
* @var array nc_panel resources for the window and its container
|
||||
*/
|
||||
var $panels = array();
|
||||
/**
|
||||
* @var array children array of child NcWindow references
|
||||
*/
|
||||
var $children = array();
|
||||
/**
|
||||
* @var NcWindow reference to this object's direct parent, if one exists
|
||||
*/
|
||||
var $parent = false;
|
||||
/**
|
||||
* @var integer Absolute column position of the window
|
||||
*/
|
||||
var $x;
|
||||
/**
|
||||
* @var integer Absolute row position of the window
|
||||
*/
|
||||
var $y;
|
||||
/**
|
||||
* @var integer Width of contained window
|
||||
*/
|
||||
var $width;
|
||||
/**
|
||||
* @var integer Height of contained window
|
||||
*/
|
||||
var $height;
|
||||
/**
|
||||
* @var string Window title
|
||||
*/
|
||||
var $title;
|
||||
|
||||
function __construct( &$parent, $height, $width, $y, $x, $framed = true ) {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Builds the requested ncurses window.
|
||||
*
|
||||
* @param NcWindow $parent Parent window
|
||||
* @param integer $height Height
|
||||
* @param integer $width Width
|
||||
* @param integer $y Row
|
||||
* @param integer $x Column
|
||||
* @param boolean $framed Include a frame (border). Required for window titles.
|
||||
*/
|
||||
public function __construct( $parent, $height, $width, $y, $x, $framed = true ) {
|
||||
if( $parent instanceof NcWindow ) {
|
||||
$this->parent = $parent;
|
||||
//TODO: Find out why this drives php nuts at script termination
|
||||
$this->parent->add_child( $this );
|
||||
$x += $parent->x;
|
||||
$y += $parent->y;
|
||||
|
@ -66,7 +152,10 @@ class NcWindow {
|
|||
$this->panels[] = ncurses_new_panel( $this->window );
|
||||
ncurses_update_panels();
|
||||
}
|
||||
function __destruct() {
|
||||
/**
|
||||
* Destructor
|
||||
*/
|
||||
public function __destruct() {
|
||||
foreach( $this->panels as $id => $panel ) {
|
||||
ncurses_del_panel( $this->panels[$id] );
|
||||
}
|
||||
|
@ -75,7 +164,16 @@ class NcWindow {
|
|||
ncurses_delwin( $this->frame );
|
||||
}
|
||||
}
|
||||
function title( $value = false ) {
|
||||
/**
|
||||
* Set or retrieve a window title
|
||||
*
|
||||
* Sets window title to the value provided, returns the current window title
|
||||
* if one is not.
|
||||
*
|
||||
* @param string $value New window title
|
||||
* @return string Window title
|
||||
*/
|
||||
public function title( $value = false ) {
|
||||
if( $value === false ) {
|
||||
return $this->title;
|
||||
} elseif( $this->frame !== false ) {
|
||||
|
@ -86,13 +184,37 @@ class NcWindow {
|
|||
ncurses_doupdate();
|
||||
}
|
||||
}
|
||||
function attribute_on( $attribute ) {
|
||||
/**
|
||||
* Activate an ncurses output attribute
|
||||
*
|
||||
* @param integer NCURSES_A_* constant
|
||||
*/
|
||||
public function attribute_on( $attribute ) {
|
||||
ncurses_wattron( $this->window, $attribute );
|
||||
}
|
||||
function attribute_off( $attribute ) {
|
||||
/**
|
||||
* Deactivate an ncurses output attribute
|
||||
*
|
||||
* @param integer NCURSES_A_* constant
|
||||
*/
|
||||
public function attribute_off( $attribute ) {
|
||||
ncurses_wattroff( $this->window, $attribute );
|
||||
}
|
||||
function write( $y, $x, $text, $update = true ) {
|
||||
/**
|
||||
* Write text to the window
|
||||
*
|
||||
* Writes text to the window at the specified coordinates.
|
||||
* Text is truncated to prevent it overflowing off the side of the window. If
|
||||
* the text contains newlines, it is split and each new line will continue from
|
||||
* the same x position as the first, rather than wrapping back to the far left
|
||||
* side of the window as ncurses normally would.
|
||||
*
|
||||
* @param integer $y Row
|
||||
* @param integer $x Column
|
||||
* @param string $text Text to write
|
||||
* @param boolean $update Update the window upon completion
|
||||
*/
|
||||
public function write( $y, $x, $text, $update = true ) {
|
||||
$y = $y < 0 ? $this->height + $y : $y;
|
||||
$x = $x < 0 ? $this->width + $x : $x;
|
||||
$lines = preg_split( '/\r?(\n|\r)/', $text );
|
||||
|
@ -105,7 +227,16 @@ class NcWindow {
|
|||
ncurses_doupdate();
|
||||
}
|
||||
}
|
||||
function write_centered( $y, $text, $update = true ) {
|
||||
/**
|
||||
* Write centered text
|
||||
*
|
||||
* Writes text centered horizontally within the window on the specified line.
|
||||
*
|
||||
* @param integer $y Row
|
||||
* @param string $text Text to write
|
||||
* @param boolean $update Update the window upon completion
|
||||
*/
|
||||
public function write_centered( $y, $text, $update = true ) {
|
||||
$this->write(
|
||||
$y,
|
||||
floor( $this->width / 2 ) - floor( strlen( $text ) / 2 ),
|
||||
|
@ -113,20 +244,55 @@ class NcWindow {
|
|||
$update
|
||||
);
|
||||
}
|
||||
function write_fixed( $y, $x, $text, $width = 0, $update = true ) {
|
||||
/**
|
||||
* Write fixed-width text
|
||||
*
|
||||
* Writes a string truncated or padded with spaces to fill the requested width.
|
||||
* If the width is unspecified or zero, the width is defined as the length from
|
||||
* the starting coordinate to the right edge of the window.
|
||||
*
|
||||
* @param integer $y Row
|
||||
* @param integer $x Column
|
||||
* @param string $text Text to write
|
||||
* @param integer $width Field length
|
||||
* @param boolean $update Update the window upon completion
|
||||
*/
|
||||
public function write_fixed( $y, $x, $text, $width = 0, $update = true ) {
|
||||
$width = $width > 0 ? $width : $this->width - $x;
|
||||
$this->write( $y, $x, $this->_string_fixed( $text, $width ), $update );
|
||||
}
|
||||
function _string_fixed( $text, $width = 0 ) {
|
||||
/**
|
||||
* Build a fixed-width string
|
||||
*
|
||||
* Truncates a string if it exceeds the specified width, pads it with spaces
|
||||
* if it is too short.
|
||||
*
|
||||
* @param string $text Text to format
|
||||
* @param integer $width Field length
|
||||
* @return string Fixed-width string
|
||||
*/
|
||||
protected function _string_fixed( $text, $width = 0 ) {
|
||||
$width = $width < 0 ? 0 : $width;
|
||||
$text = substr( $text, 0, $width );
|
||||
$text .= str_repeat( ' ', $width - strlen( $text ) );
|
||||
return $text;
|
||||
}
|
||||
function add_child( $child ) {
|
||||
/**
|
||||
* Assign a window as a child of this window
|
||||
*
|
||||
* Children of a window are hidden and shown along with their parent widget.
|
||||
*
|
||||
* @param NcWindow $child Child window
|
||||
*/
|
||||
public function add_child( $child ) {
|
||||
$this->children[] = $child;
|
||||
}
|
||||
function hide( $update = true ) {
|
||||
/**
|
||||
* Hide the window
|
||||
*
|
||||
* @param boolean $update Update the window upon completion
|
||||
*/
|
||||
public function hide( $update = true ) {
|
||||
foreach( $this->panels as $panel ) {
|
||||
ncurses_hide_panel( $panel );
|
||||
}
|
||||
|
@ -138,22 +304,42 @@ class NcWindow {
|
|||
ncurses_doupdate();
|
||||
}
|
||||
}
|
||||
function show( $update = true ) {
|
||||
/**
|
||||
* Show the window
|
||||
*
|
||||
* @param boolean $update Update the window upon completion
|
||||
*/
|
||||
public function show( $update = true ) {
|
||||
foreach( $this->panels as $panel ) {
|
||||
ncurses_show_panel( $panel );
|
||||
}
|
||||
foreach( $this->children as $child ) {
|
||||
$child->show( false );
|
||||
}
|
||||
ncurses_update_panels();
|
||||
ncurses_doupdate();
|
||||
if( $update ) {
|
||||
ncurses_update_panels();
|
||||
ncurses_doupdate();
|
||||
}
|
||||
}
|
||||
function erase() {
|
||||
/**
|
||||
* Clear the window's contents
|
||||
*
|
||||
* @param boolean $update Update the window upon completion
|
||||
*/
|
||||
public function erase( $update = true ) {
|
||||
ncurses_werase( $this->window );
|
||||
ncurses_wrefresh( $this->window );
|
||||
ncurses_doupdate();
|
||||
if( $update ) {
|
||||
ncurses_wrefresh( $this->window );
|
||||
ncurses_doupdate();
|
||||
}
|
||||
}
|
||||
function get_char( $flush = true ) {
|
||||
/**
|
||||
* Read a character from the keyboard and return it
|
||||
*
|
||||
* @param boolean $flush Flush the input buffer before requesting a key
|
||||
* @return integer Keycode
|
||||
*/
|
||||
public function get_char( $flush = true ) {
|
||||
if( $flush ) {
|
||||
ncurses_flushinp();
|
||||
}
|
||||
|
@ -162,18 +348,50 @@ class NcWindow {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress Bar
|
||||
*
|
||||
* @package Nc
|
||||
*/
|
||||
class NcProgressBar extends NcWindow {
|
||||
/**
|
||||
* @var float Maximum value
|
||||
*/
|
||||
var $max;
|
||||
/**
|
||||
* @var float Current value
|
||||
*/
|
||||
var $value;
|
||||
/**
|
||||
* @var boolean Show percentage
|
||||
*/
|
||||
var $show_pct;
|
||||
|
||||
function __construct( &$parent, $width, $y, $x, $max = 100, $show_pct = true ) {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Creates a new progress bar widget
|
||||
*
|
||||
* @param NcWindow $parent Parent window
|
||||
* @param integer $width Width
|
||||
* @param integer $y Row
|
||||
* @param integer $x Column
|
||||
* @param float $max Maximum value
|
||||
* @param boolean $show_pct Display the percentage alongside the progress bar
|
||||
*/
|
||||
public function __construct( &$parent, $width, $y, $x, $max = 100, $show_pct = true ) {
|
||||
parent::__construct( $parent, 1, $width, $y, $x, false );
|
||||
$this->max = $max;
|
||||
$this->show_pct = $show_pct;
|
||||
$this->pos( 0 );
|
||||
}
|
||||
function pos( $value = false ) {
|
||||
/**
|
||||
* Set or get the current value
|
||||
*
|
||||
* @param float $value New value
|
||||
* @return float Current value
|
||||
*/
|
||||
public function pos( $value = false ) {
|
||||
if( $value === false ) {
|
||||
return $this->value;
|
||||
} else {
|
||||
|
@ -193,7 +411,13 @@ class NcProgressBar extends NcWindow {
|
|||
ncurses_doupdate();
|
||||
}
|
||||
}
|
||||
function max( $value = false ) {
|
||||
/**
|
||||
* Get or set maximum value
|
||||
*
|
||||
* @param float $value New maximum
|
||||
* @return float Current maximum
|
||||
*/
|
||||
public function max( $value = false ) {
|
||||
if( $value === false ) {
|
||||
return $this->max;
|
||||
} else {
|
||||
|
@ -203,18 +427,49 @@ class NcProgressBar extends NcWindow {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text Input
|
||||
*
|
||||
* @package Nc
|
||||
*/
|
||||
class NcTextInput extends NcWindow {
|
||||
/**
|
||||
* @var string Current value
|
||||
*/
|
||||
var $value = '';
|
||||
/**
|
||||
* @var boolean Active
|
||||
*/
|
||||
var $active = false;
|
||||
/**
|
||||
* @var string Perl regular expression for filtering input
|
||||
*/
|
||||
var $filter = false;
|
||||
|
||||
function __construct( &$parent, $width, $y, $x, $value = '', $filter = false ) {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Creates a new text input widget
|
||||
*
|
||||
* @param NcWindow $parent Parent window
|
||||
* @param integer $width Width
|
||||
* @param integer $y Row
|
||||
* @param integer $x Column
|
||||
* @param string $value Initial value
|
||||
* @param string $filter Perl regular expression for filtering input
|
||||
*/
|
||||
public function __construct( &$parent, $width, $y, $x, $value = '', $filter = false ) {
|
||||
parent::__construct( $parent, 1, $width, $y, $x, false );
|
||||
$this->value = $value;
|
||||
$this->filter = $filter !== false && @preg_match( $filter, '' ) !== false ? $filter : false;
|
||||
$this->update();
|
||||
}
|
||||
function update() {
|
||||
/**
|
||||
* Update the input control on the screen
|
||||
*
|
||||
* Widget is highlighted when active
|
||||
*/
|
||||
public function update() {
|
||||
$this->attribute_on( NCURSES_A_UNDERLINE );
|
||||
if( $this->active ) {
|
||||
$this->attribute_on( NCURSES_A_REVERSE );
|
||||
|
@ -223,14 +478,26 @@ class NcTextInput extends NcWindow {
|
|||
$this->attribute_off( NCURSES_A_UNDERLINE );
|
||||
$this->attribute_off( NCURSES_A_REVERSE );
|
||||
}
|
||||
function value( $value = false ) {
|
||||
/**
|
||||
* Get or set value
|
||||
*/
|
||||
public function value( $value = false ) {
|
||||
if( $value === false ) {
|
||||
return $this->value;
|
||||
} else {
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
function get() {
|
||||
/**
|
||||
* Activate input widget and accept input
|
||||
*
|
||||
* Input ends when the user presses the enter key.
|
||||
* If a filter has been set and the input does not pass the filter, the value
|
||||
* reverts to the previous value, and this function returns false.
|
||||
*
|
||||
* @return string|boolean Text entered, or false
|
||||
*/
|
||||
public function get() {
|
||||
$this->active = true;
|
||||
$this->update();
|
||||
while( $this->active ) {
|
||||
|
@ -254,18 +521,56 @@ class NcTextInput extends NcWindow {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Table View
|
||||
*
|
||||
* @package Nc
|
||||
*/
|
||||
class NcTableView extends NcWindow {
|
||||
/**
|
||||
* @var array Column headings
|
||||
*/
|
||||
var $columns;
|
||||
/**
|
||||
* @var array Recordset
|
||||
*/
|
||||
var $records;
|
||||
/**
|
||||
* @var integer Currently selected record index
|
||||
*/
|
||||
var $selected;
|
||||
|
||||
function __construct( &$parent, $height, $width, $y, $x, $params = array() ) {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Creates a new table view
|
||||
*
|
||||
* @param NcWindow $parent Parent window
|
||||
* @param integer $height Height
|
||||
* @param integer $width Width
|
||||
* @param integer $y Row
|
||||
* @param integer $x Column
|
||||
* @param array Array of optional parameters, as follows:
|
||||
* <code>array(
|
||||
* 'columns' => array( 'key' => 'Column Name', ... ),
|
||||
* 'records' => array( array( 'key' => 'value', ... ), ... )
|
||||
* )</code>
|
||||
*/
|
||||
public function __construct( $parent, $height, $width, $y, $x, $params = array() ) {
|
||||
parent::__construct( $parent, $height, $width, $y, $x );
|
||||
$params = !is_array( $params ) ? array() : $params;
|
||||
$this->columns = isset( $params['columns'] ) && is_array( $params['columns'] ) ? $params['columns'] : array();
|
||||
$this->records = isset( $params['records'] ) && is_array( $params['records'] ) ? array_values( $params['records'] ) : array();
|
||||
}
|
||||
function update( $selected = 0 ) {
|
||||
/**
|
||||
* Redraw the table and select a record
|
||||
*
|
||||
* Redraws the table with the specified record selected and the view scrolled
|
||||
* accordingly.
|
||||
*
|
||||
* @param integer $selected Selected row
|
||||
*/
|
||||
public function update( $selected = 0 ) {
|
||||
$selected = $selected >= 0 ? $selected : 0;
|
||||
$selected = $selected < count( $this->records ) ? $selected : count( $this->records ) - 1;
|
||||
$this->selected = $selected;
|
||||
|
@ -333,16 +638,27 @@ class NcTableView extends NcWindow {
|
|||
ncurses_wvline( $this->window, NCURSES_ACS_VLINE, 1 );
|
||||
$this->attribute_off( NCURSES_A_UNDERLINE );
|
||||
// Highlighted Row
|
||||
ncurses_wmove( $this->window, $selected - $offset + 1, $x );
|
||||
$this->attribute_on( NCURSES_A_REVERSE );
|
||||
ncurses_wvline( $this->window, NCURSES_ACS_VLINE, 1 );
|
||||
$this->attribute_off( NCURSES_A_REVERSE );
|
||||
if( count( $this->records ) > 0 ) {
|
||||
ncurses_wmove( $this->window, $selected - $offset + 1, $x );
|
||||
$this->attribute_on( NCURSES_A_REVERSE );
|
||||
ncurses_wvline( $this->window, NCURSES_ACS_VLINE, 1 );
|
||||
$this->attribute_off( NCURSES_A_REVERSE );
|
||||
}
|
||||
}
|
||||
// Finally, update
|
||||
ncurses_wrefresh( $this->window );
|
||||
ncurses_doupdate();
|
||||
}
|
||||
function load( $records ) {
|
||||
/**
|
||||
* Load a recordset
|
||||
*
|
||||
* Recordset should be an array in the following format:
|
||||
* <code>array( array( 'key' => 'value', ... ), ... )</code>
|
||||
*
|
||||
* @param array $records Recordset
|
||||
* @return boolean True if recordset loaded successfully, false otherwise
|
||||
*/
|
||||
public function load( $records ) {
|
||||
if( !is_array( $records ) ) {
|
||||
return false;
|
||||
}
|
||||
|
|
24
scanner.php
24
scanner.php
|
@ -368,17 +368,19 @@ if( $curses ) {
|
|||
break;
|
||||
case 10:
|
||||
case 13:
|
||||
$nc_fault_info = new NcWindow( $nc_main, -10, -10, 5, 5 );
|
||||
$fault = &$faults[$nc_faults->selected];
|
||||
$nc_fault_info->attribute_on( NCURSES_A_BOLD );
|
||||
$nc_fault_info->write_centered( 0, 'Fault Information' );
|
||||
$nc_fault_info->write( 3, 0, "Module:\nLevel:\nFilename:\nLine Number:\nAuthor:\nRevision:\nReason:\n\nContext:" );
|
||||
$nc_fault_info->attribute_off( NCURSES_A_BOLD );
|
||||
$fault['object']['context'] = trim( $fault['object']['context'] );
|
||||
$nc_fault_info->write( 3, 20, "{$fault['module']}\n{$fault['level']}\n{$fault['file']}\n{$fault['line']}\n{$fault['author']}\n{$fault['revision']}\n{$fault['reason']}\n\n{$fault['object']['context']}" );
|
||||
$nc_fault_info->get_char();
|
||||
$nc_fault_info->hide();
|
||||
unset( $nc_fault_info );
|
||||
if( count( $nc_faults->records ) > 0 ) {
|
||||
$nc_fault_info = new NcWindow( $nc_main, -10, -10, 5, 5 );
|
||||
$fault = &$faults[$nc_faults->selected];
|
||||
$nc_fault_info->attribute_on( NCURSES_A_BOLD );
|
||||
$nc_fault_info->write_centered( 0, 'Fault Information' );
|
||||
$nc_fault_info->write( 3, 0, "Module:\nLevel:\nFilename:\nLine Number:\nAuthor:\nRevision:\nReason:\n\nContext:" );
|
||||
$nc_fault_info->attribute_off( NCURSES_A_BOLD );
|
||||
$fault['object']['context'] = trim( $fault['object']['context'] );
|
||||
$nc_fault_info->write( 3, 20, "{$fault['module']}\n{$fault['level']}\n{$fault['file']}\n{$fault['line']}\n{$fault['author']}\n{$fault['revision']}\n{$fault['reason']}\n\n{$fault['object']['context']}" );
|
||||
$nc_fault_info->get_char();
|
||||
$nc_fault_info->hide();
|
||||
unset( $nc_fault_info );
|
||||
}
|
||||
break;
|
||||
case ord( 's' ):
|
||||
case ord( 'S' );
|
||||
|
|
Loading…
Reference in a new issue